diff --git a/docs/index.html b/docs/index.html index 355c052..e674024 100644 --- a/docs/index.html +++ b/docs/index.html @@ -35,7 +35,14 @@

Modules

  • data
  • polycore
  • util
  • -
  • widget
  • +
  • widget_core
  • +
  • widget_cpu
  • +
  • widget_drive
  • +
  • widget_gpu
  • +
  • widget_graph
  • +
  • widget_memory
  • +
  • widget_network
  • +
  • widget_text
  • @@ -64,7 +71,35 @@

    Modules

    Various helper functions - widget + widget_core + A collection of Widget classes + + + widget_cpu + A collection of CPU Widget classes + + + widget_drive + A collection of Disk Drive Widget classes + + + widget_gpu + A collection of GPU Widget classes + + + widget_graph + A collection of Basic Graph and indicator Widget classes + + + widget_memory + A collection of Widget classes + + + widget_network + A collection of Network Widget classes + + + widget_text A collection of Widget classes @@ -73,7 +108,7 @@

    Modules

    generated by LDoc 1.4.6 -Last updated 2023-03-26 23:57:51 +Last updated 2023-05-03 21:59:22
    diff --git a/docs/modules/cairo_helpers.html b/docs/modules/cairo_helpers.html index 6d12564..dfd278a 100644 --- a/docs/modules/cairo_helpers.html +++ b/docs/modules/cairo_helpers.html @@ -43,7 +43,14 @@

    Modules

  • data
  • polycore
  • util
  • -
  • widget
  • +
  • widget_core
  • +
  • widget_cpu
  • +
  • widget_drive
  • +
  • widget_gpu
  • +
  • widget_graph
  • +
  • widget_memory
  • +
  • widget_network
  • +
  • widget_text
  • @@ -646,7 +653,7 @@

    Parameters:

    generated by LDoc 1.4.6 -Last updated 2023-03-26 23:57:51 +Last updated 2023-05-01 20:49:18
    diff --git a/docs/modules/data.html b/docs/modules/data.html index 9128006..258f26c 100644 --- a/docs/modules/data.html +++ b/docs/modules/data.html @@ -43,7 +43,14 @@

    Modules

  • data
  • polycore
  • util
  • -
  • widget
  • +
  • widget_core
  • +
  • widget_cpu
  • +
  • widget_drive
  • +
  • widget_gpu
  • +
  • widget_graph
  • +
  • widget_memory
  • +
  • widget_network
  • +
  • widget_text
  • @@ -742,7 +749,7 @@

    Parameters:

    generated by LDoc 1.4.6 -Last updated 2023-03-26 23:57:51 +Last updated 2023-05-01 20:49:18
    diff --git a/docs/modules/polycore.html b/docs/modules/polycore.html index 15eff51..a47b0c7 100644 --- a/docs/modules/polycore.html +++ b/docs/modules/polycore.html @@ -44,7 +44,14 @@

    Modules

  • data
  • polycore
  • util
  • -
  • widget
  • +
  • widget_core
  • +
  • widget_cpu
  • +
  • widget_drive
  • +
  • widget_gpu
  • +
  • widget_graph
  • +
  • widget_memory
  • +
  • widget_network
  • +
  • widget_text
  • @@ -245,7 +252,7 @@

    Parameters:

    generated by LDoc 1.4.6 -Last updated 2023-03-26 23:57:51 +Last updated 2023-05-01 20:49:18
    diff --git a/docs/modules/util.html b/docs/modules/util.html index a712e31..ebd5732 100644 --- a/docs/modules/util.html +++ b/docs/modules/util.html @@ -45,7 +45,14 @@

    Modules

  • data
  • polycore
  • util
  • -
  • widget
  • +
  • widget_core
  • +
  • widget_cpu
  • +
  • widget_drive
  • +
  • widget_gpu
  • +
  • widget_graph
  • +
  • widget_memory
  • +
  • widget_network
  • +
  • widget_text
  • @@ -771,7 +778,7 @@

    Parameters:

    generated by LDoc 1.4.6 -Last updated 2023-03-26 23:57:51 +Last updated 2023-05-01 20:49:18
    diff --git a/docs/modules/widget_core.html b/docs/modules/widget_core.html new file mode 100644 index 0000000..2b494b3 --- /dev/null +++ b/docs/modules/widget_core.html @@ -0,0 +1,718 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + +

    Module widget_core

    +

    A collection of Widget classes

    +

    + + +

    Functions

    + + + + + +
    w.temperature_color(temperature, low, high)Generate a temperature based color.
    +

    Tables

    + + + + + + + + + +
    default_text_colorText color used by widgets if no other is specified.
    default_graph_colorColor used to draw some widgets if no other is specified.
    +

    Fields

    + + + + + + + + + +
    default_font_familyFont used by widgets if no other is specified.
    default_font_sizeFont size used by widgets if no other is specified.
    +

    Class Renderer

    + + + + + + + + + + + + + + + + + +
    Renderer:init(args)
    Renderer:layout()Layout all Widgets and cache their backgrounds.
    Renderer:update(update_count)Update all Widgets
    Renderer:render(cr)Render to the given context
    +

    Class Widget

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Widget.widthSet a width if the Widget should have a fixed width.
    Widget.heightSet a height if the Widget should have a fixed height.
    Widget:layout(width, height)Called at least once to inform the widget of the width and height + it may occupy.
    Widget:render_background(cr)Called at least once to allow the widget to draw static content.
    Widget:update(update_count)Called before each call to Widget:render.
    Widget:render(cr)Called once per update to do draw dynamic content.
    +

    Class Rows

    + + + + + +
    Rows:init(widgets)
    +

    Class Columns

    + + + + + +
    Columns:init(widgets)
    +

    Class Filler

    + + + + + +
    Filler:init(args)
    +

    Class Frame

    + + + + + +
    Frame:init(widget, args)
    + +
    +
    + + +

    Functions

    + +
    +
    + + w.temperature_color(temperature, low, high) +
    +
    + Generate a temperature based color. + Colors are chosen based on float offset in a pre-defined color gradient. + + +

    Parameters:

    +
      +
    • temperature + number + current temperature (or any other type of numeric value) +
    • +
    • low + number + threshold for lowest temperature / coolest color +
    • +
    • high + number + threshold for highest temperature / hottest color +
    • +
    + + + + + +
    +
    +

    Tables

    + +
    +
    + + default_text_color +
    +
    + Text color used by widgets if no other is specified. + + +

    Fields:

    +
      +
    • default_text_color + {number,number,number,number} + +
    • +
    + + + + + +
    +
    + + default_graph_color +
    +
    + Color used to draw some widgets if no other is specified. + + +

    Fields:

    +
      +
    • default_graph_color + {number,number,number,number} + +
    • +
    + + + + + +
    +
    +

    Fields

    + +
    +
    + + default_font_family +
    +
    + Font used by widgets if no other is specified. + + +
      +
    • default_font_family + string + +
    • +
    + + + + + +
    +
    + + default_font_size +
    +
    + Font size used by widgets if no other is specified. + + +
      +
    • default_font_size + int + +
    • +
    + + + + + +
    +
    +

    Class Renderer

    + +
    + Root widget wrapper + Takes care of managing layout reflows and background caching. +
    +
    +
    + + Renderer:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • root + Widget + The Widget subclass that should be rendered, + usually a Rows widget +
      • +
      • width + int + Width of the surface that should be covered +
      • +
      • height + int + Height of the surface that should be covered +
      • +
      +
    + + + + + +
    +
    + + Renderer:layout() +
    +
    + Layout all Widgets and cache their backgrounds. + Call this once to create the initial layout. + Will be called again automatically each time the layout changes. + + + + + + + +
    +
    + + Renderer:update(update_count) +
    +
    + Update all Widgets + + +

    Parameters:

    +
      +
    • update_count + int + Conky's $updates +
    • +
    + + + + + +
    +
    + + Renderer:render(cr) +
    +
    + Render to the given context + + +

    Parameters:

    +
      +
    • cr + cairo_t + +
    • +
    + + + + + +
    +
    +

    Class Widget

    + +
    + Base Widget class. +
    +
    +
    + + Widget.width +
    +
    + Set a width if the Widget should have a fixed width. + Omit (=nil) if width should be adjusted dynamically. + + + + + + + +
    +
    + + Widget.height +
    +
    + Set a height if the Widget should have a fixed height. + Omit (=nil) if height should be adjusted dynamically. + + + + + + + +
    +
    + + Widget:layout(width, height) +
    +
    + Called at least once to inform the widget of the width and height + it may occupy. + + +

    Parameters:

    +
      +
    • width + int + +
    • +
    • height + int + +
    • +
    + + + + + +
    +
    + + Widget:render_background(cr) +
    +
    + Called at least once to allow the widget to draw static content. + + +

    Parameters:

    +
      +
    • cr + cairo_t + Cairo context for background rendering + (to be cached by the Renderer) +
    • +
    + + + + + +
    +
    + + Widget:update(update_count) +
    +
    + Called before each call to Widget:render. + If this function returns a true-ish value, a reflow will be triggered. + Since this involves calls to all widgets' :layout functions, + reflows should be used sparingly. + + +

    Parameters:

    +
      +
    • update_count + int + Conky's $updates +
    • +
    + +

    Returns:

    +
      + + optional bool + true(-ish) if a layout reflow should be triggered, causing + all Widget:layout and Widget:render_background methods + to be called again +
    + + + + +
    +
    + + Widget:render(cr) +
    +
    + Called once per update to do draw dynamic content. + + +

    Parameters:

    +
      +
    • cr + cairo_t + +
    • +
    + + + + + +
    +
    +

    Class Rows

    + +
    + Basic collection of widgets. + Rows are drawn in a vertical stack starting at the top of the drawble + surface. +
    +
    +
    + + Rows:init(widgets) +
    +
    + + + +

    Parameters:

    + + + + + + +
    +
    +

    Class Columns

    + +
    + Display Widgets side by side +
    +
    +
    + + Columns:init(widgets) +
    +
    + + + +

    Parameters:

    + + + + + + +
    +
    +

    Class Filler

    + +
    + Leave space between widgets. + If either height or width is not specified, the available space + inside a Rows or Columns widget will be distributed evenly between Fillers + with no fixed height/width. + A Filler may contain one other Widget which will have its dimensions + restricted to those of the Filler. +
    +
    +
    + + Filler:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • width + optional int + +
      • +
      • height + optional int + +
      • +
      • widget + optional Widget + +
      • +
      +
    + + + + + +
    +
    +

    Class Frame

    + +
    + Draw a static border and/or background around/behind another widget. +
    +
    +
    + + Frame:init(widget, args) +
    +
    + + + +

    Parameters:

    +
      +
    • widget + Widget + Widget to be wrapped +
    • +
    • args table of options +
        +
      • padding + number or {number,...} + Leave some space around the inside + of the frame.
        + - number: same padding all around.
        + - table of two numbers: {top & bottom, left & right}
        + - table of three numbers: {top, left & right, bottom}
        + - table of four numbers: {top, right, bottom, left} +
      • +
      • margin + number or {number,...} + Like padding but outside the border. +
      • +
      • background_color + optional {number,number,number,number} + +
      • +
      • border_color + optional {number,number,number,number} + + (default transparent) +
      • +
      • border_width + optional number + border line width + (default 0) +
      • +
      • border_sides + optional {string,...} + any combination of + "top", "right", "bottom" and/or "left" + (default: all sides) +
      • +
      +
    + + + + + +
    +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2023-05-01 20:49:18 +
    +
    + + diff --git a/docs/modules/widget_cpu.html b/docs/modules/widget_cpu.html new file mode 100644 index 0000000..61bbc0e --- /dev/null +++ b/docs/modules/widget_cpu.html @@ -0,0 +1,235 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + +

    Module widget_cpu

    +

    A collection of CPU Widget classes

    +

    + + +

    Class Cpu

    + + + + + +
    Cpu:init(args)
    +

    Class CpuRound

    + + + + + +
    CpuRound:init(args)
    +

    Class CpuFrequencies

    + + + + + +
    CpuFrequencies:init(args)
    + +
    +
    + + +

    Class Cpu

    + +
    + Polygon-style CPU usage & temperature indicator. + Looks best for CPUs with 4 to 8 cores but also works for higher numbers. +
    +
    +
    + + Cpu:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • cores + int + How many cores does your CPU have? +
      • +
      • scale + int + radius of central polygon +
      • +
      • gap + int + space between central polygon and outer segments +
      • +
      • segment_size + int + radial thickness of outer segments +
      • +
      +
    + + + + + +
    +
    +

    Class CpuRound

    + +
    + Round CPU usage & temperature indicator. + Best suited for CPUs with high core counts. +
    +
    +
    + + CpuRound:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • cores + int + How many cores does your CPU have? +
      • +
      • inner_radius + int + Size of inner circle +
      • +
      • outer_radius + int + Max radius for core at 100% +
      • +
      • grid + int + Number of grid lines to draw in the background. + (optional) +
      • +
      +
    + + + + + +
    +
    +

    Class CpuFrequencies

    + +
    + Visualize cpu-frequencies in a style reminiscent of stacked progress bars. +
    +
    +
    + + CpuFrequencies:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • cores + int + How many cores does your CPU have? +
      • +
      • min_freq + number + What is your CPU's maximum frequency? +
      • +
      • min_freq + number + What is your CPU's maximum frequency? +
      • +
      • height + int + Maximum pixel height of the drawn shape + (default 16) +
      • +
      +
    + + + + + +
    +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2023-05-01 20:49:18 +
    +
    + + diff --git a/docs/modules/widget_drive.html b/docs/modules/widget_drive.html new file mode 100644 index 0000000..3cf44ec --- /dev/null +++ b/docs/modules/widget_drive.html @@ -0,0 +1,116 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + +

    Module widget_drive

    +

    A collection of Disk Drive Widget classes

    +

    + + +

    Class Drive

    + + + + + +
    Drive:init(path)
    + +
    +
    + + +

    Class Drive

    + +
    + Visualize drive usage and temperature in a colorized Bar. + Also writes temperature as text. + This widget is exptected to be combined with some special conky.text. +
    +
    +
    + + Drive:init(path) +
    +
    + + + +

    Parameters:

    +
      +
    • path + string + e.g. "/home" +
    • +
    + + + + + +
    +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2023-05-01 20:49:18 +
    +
    + + diff --git a/docs/modules/widget_gpu.html b/docs/modules/widget_gpu.html new file mode 100644 index 0000000..34f74f9 --- /dev/null +++ b/docs/modules/widget_gpu.html @@ -0,0 +1,159 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + +

    Module widget_gpu

    +

    A collection of GPU Widget classes

    +

    + + +

    Class Gpu

    + + + + + +
    Gpu:init()no options
    +

    Class GpuTop

    + + + + + +
    GpuTop:init(args)
    + +
    +
    + + +

    Class Gpu

    + +
    + Compound widget to display GPU and VRAM usage. +
    +
    +
    + + Gpu:init() +
    +
    + no options + + + + + + + +
    +
    +

    Class GpuTop

    + +
    + Table of processes for the GPU, sorted by VRAM usage +
    +
    +
    + + GpuTop:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • lines + optional int + how many processes to display + (default 5) +
      • +
      • font_family + optional string + +
      • +
      • font_size + optional number + +
      • +
      • color + optional {number,number,number} + (default: default_text_color) +
      • +
      +
    + + + + + +
    +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2023-05-01 20:49:18 +
    +
    + + diff --git a/docs/modules/widget_graph.html b/docs/modules/widget_graph.html new file mode 100644 index 0000000..a97a86a --- /dev/null +++ b/docs/modules/widget_graph.html @@ -0,0 +1,367 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + +

    Module widget_graph

    +

    A collection of Basic Graph and indicator Widget classes

    +

    + + +

    Class Bar

    + + + + + + + + + +
    Bar:init(args)
    Bar:set_fill(fraction)Set the fill-ratio of the bar
    +

    Class Graph

    + + + + + + + + + +
    Graph:init(args)
    Graph:add_value(value)Append the latest value to be shown - this will displace the oldest value
    +

    Class LED

    + + + + + + + + + + + + + +
    LED:init(args)
    LED:set_brightness(brightness)
    LED:set_color(color)
    + +
    +
    + + +

    Class Bar

    + +
    + Progress-bar like box, similar to conky's bar. + Can have small and big ticks for visual clarity, + and a unit (static, up to 3 characters) written behind the end. +
    +
    +
    + + Bar:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • thickness + int + vertical size of the bar + (default 6) +
      • +
      • unit + optional string + to be drawn behind the bar - 3 characters will fit +
      • +
      • ticks + optional {number,...} + relative offsets (between 0 and 1) of ticks +
      • +
      • big_ticks + optional int + multiple of ticks to be drawn longer +
      • +
      • color + optional {number,number,number} + (default: default_graph_color) +
      • +
      +
    + + + + + +
    +
    + + Bar:set_fill(fraction) +
    +
    + Set the fill-ratio of the bar + + +

    Parameters:

    +
      +
    • fraction + number + between 0 and 1 +
    • +
    + + + + + +
    +
    +

    Class Graph

    + +
    + Track changing data; similar to conky's graphs. +
    +
    +
    + + Graph:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • max + number + maximum expected value to be represented; + may be expanded automatically as need arises +
      • +
      • data_points + int + how many values to store + (default 60) +
      • +
      • upside_down + bool + Draw graph from top to bottom? + (default false) +
      • +
      • smoothness + number + Bézier curves smoothness. + Set to 0 to draw straight lines instead, + which may be slightly faster. + (default 0.5) +
      • +
      • width + int + fix width in pixels + (optional) +
      • +
      • height + int + fixeheight in pixels + (optional) +
      • +
      • color + optional {number,number,number} + (default: default_graph_color) +
      • +
      +
    + + + + + +
    +
    + + Graph:add_value(value) +
    +
    + Append the latest value to be shown - this will displace the oldest value + + +

    Parameters:

    +
      +
    • value + number + if value > args.max then the graphs vertical scale will be + adjusted, causing it to get squished +
    • +
    + + + + + +
    +
    +

    Class LED

    + +
    + Round light indicator for minimalistic feedback. +
    +
    +
    + + LED:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • radius + number + size of the LED +
      • +
      • brightness + number + between 0 and 1, how "on" should the LED be? + Can be changed later with LED:set_brightness + (default 0) +
      • +
      • color + optional {number,number,number} + color of the LED, + can be changed later with LED:set_color. + (default: default_graph_color) +
      • +
      • background_color + optional {number,number,number,number} + mostly visible + when the LED is off. This allows you to choose + a neutral background if you plan on changing + the light color via LED:set_color. + (default: darkened args.color) +
      • +
      +
    + + + + + +
    +
    + + LED:set_brightness(brightness) +
    +
    + + + +

    Parameters:

    +
      +
    • brightness + number + between 0 and 1 +
    • +
    + + + + + +
    +
    + + LED:set_color(color) +
    +
    + + + +

    Parameters:

    +
      +
    • color + optional {number,number,number} + +
    • +
    + + + + + +
    +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2023-05-01 20:49:18 +
    +
    + + diff --git a/docs/modules/widget_memory.html b/docs/modules/widget_memory.html new file mode 100644 index 0000000..026a4fb --- /dev/null +++ b/docs/modules/widget_memory.html @@ -0,0 +1,224 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + +

    Module widget_memory

    +

    A collection of Widget classes

    +

    + + +

    Class MemoryBar

    + + + + + + + + + +
    MemoryBar:init(args)
    MemoryBar:set_used(used)Set the amount of used memory as an absolute value.
    +

    Class MemoryGrid

    + + + + + +
    MemoryGrid:init(args)
    + +
    +
    + + +

    Class MemoryBar

    + +
    + Specialized unit-based Bar. +
    +
    +
    + + MemoryBar:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • total + optional number + Total amount of memory to be represented + by this bar. If greater than 8, ticks will be + drawn. If omitted, total RAM will be used, + however no ticks can be drawn. +
      • +
      • unit + string + passed to Bar:init + (default "GiB") +
      • +
      • thickness + optional int + passed to Bar:init +
      • +
      • color + optional {number,number,number} + passed to Bar:init +
      • +
      +
    + + + + + +
    +
    + + MemoryBar:set_used(used) +
    +
    + Set the amount of used memory as an absolute value. + + +

    Parameters:

    +
      +
    • used + number + should be between 0 and args.total +
    • +
    + + + + + +
    +
    +

    Class MemoryGrid

    + +
    + Visualize memory usage in a randomized grid. + Does not represent actual distribution of used memory. + Also shows buffere/cache memory at reduced brightness. +
    +
    +
    + + MemoryGrid:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • rows + optional int + Number of rows to draw. + For nil it will be determined based on Widget height. +
      • +
      • columns + optional int + Number of columns to draw. + For nil it will be determined based on Widget width. +
      • +
      • point_size + optional int + edge length of individual squares + (default 2) +
      • +
      • gap + optional int + space between squares + (default 1) +
      • +
      • shuffle + optional bool + randomize? + (default true) +
      • +
      • color + optional {number,number,number} + (default: default_graph_color) +
      • +
      +
    + + + + + +
    +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2023-05-01 20:49:18 +
    +
    + + diff --git a/docs/modules/widget_network.html b/docs/modules/widget_network.html new file mode 100644 index 0000000..c86464a --- /dev/null +++ b/docs/modules/widget_network.html @@ -0,0 +1,132 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + +

    Module widget_network

    +

    A collection of Network Widget classes

    +

    + + +

    Class Network

    + + + + + +
    Network:init(args)
    + +
    +
    + + +

    Class Network

    + +
    + Graphs for up- and download speed. + This widget assumes that your conky.text adds some text between the graphs. +
    +
    +
    + + Network:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • interface + string + e.g. "eth0" +
      • +
      • graph_height + optional int + passed to Graph:init +
      • +
      • downspeed + number + passed as args.max to download speed graph + (default 1024) +
      • +
      • upspeed + number + passed as args.max to upload speed graph + (default 1024) +
      • +
      +
    + + + + + +
    +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2023-05-01 20:49:18 +
    +
    + + diff --git a/docs/modules/widget_text.html b/docs/modules/widget_text.html new file mode 100644 index 0000000..d78a107 --- /dev/null +++ b/docs/modules/widget_text.html @@ -0,0 +1,283 @@ + + + + + Reference + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + +

    Module widget_text

    +

    A collection of Widget classes

    +

    + + +

    Class Text

    + + + + + +
    Text:init(args)
    +

    Class ConkyText

    + + + + + +
    ConkyText:init(args)
    +

    Class StaticText

    + + + + + +
    StaticText:init(text, args)
    +

    Class TextLine

    + + + + + + + + + +
    TextLine:init(args)
    TextLine:set_text(text)Update the text line to be displayed.
    + +
    +
    + + +

    Class Text

    + +
    + Common (abstract) base class for StaticText and TextLine. +
    +
    +
    + + Text:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args table of options +
        +
      • align + optional string + "left" (default), "center" or "right" +
      • +
      • font_family + optional string + + (default w.default_font_family) +
      • +
      • font_size + optional number + + (default w.default_font_size) +
      • +
      • font_slant + optional cairo_font_slant_t + + (default CAIRO_FONT_SLANT_NORMAL) +
      • +
      • font_weight + optional cairo_font_weight_t + + (default CAIRO_FONT_WEIGHT_NORMAL) +
      • +
      • color + optional {number,number,number,number} + (default: default_text_color) +
      • +
      +
    + + + + + +
    +
    +

    Class ConkyText

    + +
    + Draw text substuting in Conky variables. + Text line will be updated on each cycle as per Conky's text + Section, some variables such as formatting and positioning + may not be honored. +
    +
    +
    + + ConkyText:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args + table + table of options, see Text:init +
    • +
    + + + + + +
    +
    +

    Class StaticText

    + +
    + Draw some unchangeable text. + Use this widget for text that will never be updated.Text +
    +
    +
    + + StaticText:init(text, args) +
    +
    + + + +

    Parameters:

    +
      +
    • text + string + Text to be displayed. +
    • +
    • args + optional table + table of options, see Text:init +
    • +
    + + + + + +
    +
    +

    Class TextLine

    + +
    + Draw a single line of changeable text. + Text line can be updated on each cycle via set_text. +
    +
    +
    + + TextLine:init(args) +
    +
    + + + +

    Parameters:

    +
      +
    • args + table + table of options, see Text:init +
    • +
    + + + + + +
    +
    + + TextLine:set_text(text) +
    +
    + Update the text line to be displayed. + + +

    Parameters:

    +
      +
    • text + string + +
    • +
    + + + + + +
    +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2023-05-03 21:59:22 +
    +
    + + diff --git a/examples/columns.lua b/examples/columns.lua index fb3e407..2ac6a3d 100644 --- a/examples/columns.lua +++ b/examples/columns.lua @@ -4,8 +4,15 @@ local script_dir = debug.getinfo(1, 'S').source:match("^@(.*/)") or "./" package.path = script_dir .. "../?.lua;" .. package.path -local widget = require('src/widget') local polycore = require('src/polycore') +local data = require('src/data') +local core = require('src/widgets/core') +local cpu = require('src/widgets/cpu') +local drive = require('src/widgets/drive') +local gpu = require('src/widgets/gpu') +local mem = require('src/widgets/memory') +local net = require('src/widgets/network') +local text = require('src/widgets/text') -- Draw debug information DEBUG = false @@ -15,6 +22,7 @@ local conkyrc = conky or {} conkyrc.config = { lua_load = script_dir .. "columns.lua", lua_startup_hook = "conky_setup", + lua_draw_hook_pre = "conky_paint_background", lua_draw_hook_post = "conky_update", update_interval = 1, @@ -123,45 +131,45 @@ function polycore.setup() local secondary_text_color = {.72, .72, .71, 1} -- ~b9b9b7 - local root = widget.Frame(widget.Columns{ - widget.Rows{ - widget.Filler{}, - widget.Cpu{cores=6, inner_radius=28, gap=5, outer_radius=57}, - widget.Filler{}, + local root = core.Frame(core.Columns{ + core.Rows{ + core.Filler{}, + cpu.Cpu{cores=6, inner_radius=28, gap=5, outer_radius=57}, + core.Filler{}, }, - widget.Filler{width=10}, - widget.MemoryGrid{columns=5}, - widget.Filler{width=20}, - widget.Rows{ - widget.CpuFrequencies{cores=6, min_freq=0.75, max_freq=4.3}, - widget.Filler{}, + core.Filler{width=10}, + mem.MemoryGrid{columns=5}, + core.Filler{width=20}, + core.Rows{ + cpu.CpuFrequencies{cores=6, min_freq=0.75, max_freq=4.3}, + core.Filler{}, }, - widget.Filler{width=30}, - widget.Rows{ - widget.Filler{height=5}, - widget.Gpu(), - widget.Filler{height=5}, - widget.GpuTop{lines=5, color=secondary_text_color}, + core.Filler{width=30}, + core.Rows{ + core.Filler{height=5}, + gpu.Gpu(), + core.Filler{height=5}, + gpu.GpuTop{lines=5, color=secondary_text_color}, }, - widget.Filler{width=30}, - widget.Rows{ - widget.Filler{height=26}, - widget.Network{interface="enp0s31f6", downspeed=5 * 1024, upspeed=1024}, + core.Filler{width=30}, + core.Rows{ + core.Filler{height=26}, + net.Network{interface="enp34s0u1u3u4", downspeed=5 * 1024, upspeed=1024}, }, - widget.Filler{width=30}, - widget.Rows{ - widget.Drive("/"), - widget.Filler{height=-9}, - widget.Drive("/home"), - widget.Filler{height=-9}, - widget.Drive("/mnt/blackstor"), + core.Filler{width=30}, + core.Rows{ + drive.Drive("/"), + core.Filler{height=-9}, + drive.Drive("/home"), + core.Filler{height=-9}, + drive.Drive("/mnt/blackstor") }, }, { border_color={0.8, 1, 1, 0.05}, border_width = 1, padding = {40, 20, 20, 10}, }) - return widget.Renderer{root=root, + return core.Renderer{root=root, width=conkyrc.config.minimum_width, height=conkyrc.config.minimum_height} end diff --git a/examples/cpu.lua b/examples/cpu.lua index dbf00f8..b9cc615 100644 --- a/examples/cpu.lua +++ b/examples/cpu.lua @@ -4,11 +4,11 @@ local script_dir = debug.getinfo(1, 'S').source:match("^@(.*/)") or "./" package.path = script_dir .. "../?.lua;" .. package.path -local widget = require('src/widget') local util = require('src/util') local data = require('src/data') local polycore = require('src/polycore') - +local core = require('src/widgets/core') +local cpu = require('src/widgets/cpu') local width = 500 local height = 540 @@ -31,39 +31,39 @@ function polycore.setup() end end - local root = widget.Frame(widget.Rows{ - widget.Columns{ - widget.Rows{ - widget.Cpu{cores=6, outer_radius=52, inner_radius=26, gap=5}, - widget.Filler{height=20}, - widget.Cpu{cores=10, outer_radius=52, inner_radius=30, gap=3}, + local root = core.Frame(core.Rows{ + core.Columns{ + core.Rows{ + cpu.Cpu{cores=6, outer_radius=52, inner_radius=26, gap=5}, + core.Filler{height=20}, + cpu.Cpu{cores=10, outer_radius=52, inner_radius=30, gap=3}, }, - widget.Rows{ - widget.Cpu{cores=8, outer_radius=52, inner_radius=24, gap=7}, - widget.Filler{height=20}, - widget.Cpu{cores=12, outer_radius=52, inner_radius=36, gap=5}, + core.Rows{ + cpu.Cpu{cores=8, outer_radius=52, inner_radius=24, gap=7}, + core.Filler{height=20}, + cpu.Cpu{cores=12, outer_radius=52, inner_radius=36, gap=5}, }, - widget.Filler{width=20}, - widget.Cpu{cores=6, gap=7, outer_radius=100}, + core.Filler{width=20}, + cpu.Cpu{cores=6, gap=7, outer_radius=100}, }, - widget.Filler{}, - widget.Columns{ - widget.Rows{ - widget.CpuRound{cores=6, outer_radius=52, inner_radius=26}, - widget.Filler{height=20}, - widget.CpuRound{cores=16, outer_radius=52, inner_radius=30}, + core.Filler{}, + core.Columns{ + core.Rows{ + cpu.CpuRound{cores=6, outer_radius=52, inner_radius=26}, + core.Filler{height=20}, + cpu.CpuRound{cores=16, outer_radius=52, inner_radius=30}, }, - widget.Rows{ - widget.CpuRound{cores=6, outer_radius=52, inner_radius=24, grid=5}, - widget.Filler{height=20}, - widget.CpuRound{cores=32, outer_radius=52, inner_radius=36, grid=4}, + core.Rows{ + cpu.CpuRound{cores=6, outer_radius=52, inner_radius=24, grid=5}, + core.Filler{height=20}, + cpu.CpuRound{cores=32, outer_radius=52, inner_radius=36, grid=4}, }, - widget.Filler{width=20}, - widget.CpuRound{cores=64, outer_radius=100, grid=5}, + core.Filler{width=20}, + cpu.CpuRound{cores=64, outer_radius=100, grid=5}, }, }, {padding=20}) - return widget.Renderer{root=root, width=width, height=height} + return core.Renderer{root=root, width=width, height=height} end diff --git a/examples/graphs.lua b/examples/graphs.lua index 0a89399..322d7f5 100644 --- a/examples/graphs.lua +++ b/examples/graphs.lua @@ -4,9 +4,12 @@ local script_dir = debug.getinfo(1, 'S').source:match("^@(.*/)") or "./" package.path = script_dir .. "../?.lua;" .. package.path -local widget = require('src/widget') local data = require('src/data') local polycore = require('src/polycore') +local core = require('src/widgets/core') +local graph = require('src/widgets/graph') +local text = require('src/widgets/text') + local GRAPH_SMOOTHINGS = {0, 0.2, 0.5, 0.7, 1.0} @@ -20,22 +23,22 @@ function polycore.setup() local graphs = {} local widgets = {} for _, smoothness in ipairs(GRAPH_SMOOTHINGS) do - local graph = widget.Graph{ + local graph = graph.Graph{ smoothness=smoothness, data_points=90, max=5 * 1024, } table.insert(graphs, graph) - local heading = widget.TextLine{} + local heading = text.TextLine{} heading:set_text(("Smoothness: %.1f"):format(smoothness)) - table.insert(widgets, widget.Filler{height=5}) + table.insert(widgets, core.Filler{height=5}) table.insert(widgets, heading) - table.insert(widgets, widget.Filler{height=4}) + table.insert(widgets, core.Filler{height=4}) table.insert(widgets, graph) end - local root = widget.Frame(widget.Rows(widgets), {padding={5, 10, 10}}) + local root = core.Frame(core.Rows(widgets), {padding={5, 10, 10}}) function root.update() local downspeed, _ = data.network_speed("enp0s31f6") @@ -44,7 +47,7 @@ function polycore.setup() end end - return widget.Renderer{root=root, width=width, height=height} + return core.Renderer{root=root, width=width, height=height} end diff --git a/examples/text.lua b/examples/text.lua index aa8b492..75451e0 100644 --- a/examples/text.lua +++ b/examples/text.lua @@ -4,7 +4,8 @@ local script_dir = debug.getinfo(1, 'S').source:match("^@(.*/)") or "./" package.path = script_dir .. "../?.lua;" .. package.path -local widget = require('src/widget') +local core = require('src/widgets/core') +local text = require('src/widgets/text') local polycore = require('src/polycore') local width = 400 @@ -19,14 +20,14 @@ cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]] --- Called once on startup to initialize widgets. --- @treturn widget.Renderer +-- @treturn core.Renderer function polycore.setup() local widgets = { -- heading - widget.Frame(widget.StaticText("Text Demo", { + core.Frame(text.StaticText("Text Demo", { font_size=20, font_weight=CAIRO_FONT_WEIGHT_BOLD, - color=widget.default_graph_color, + color=core.default_graph_color, }), { margin={0, 0, 10}, border_sides={"bottom"}, @@ -35,21 +36,21 @@ function polycore.setup() }), -- simple text - widget.StaticText"Hello World!", - widget.Filler{height=10}, - widget.StaticText("How are you doing?", {align="right"}), + text.StaticText"Hello World!", + core.Filler{height=10}, + text.StaticText("How are you doing?", {align="right"}), - widget.Filler(), + core.Filler(), -- paragraph with newlines - widget.StaticText(LOREM_IPSUM, { + text.StaticText(LOREM_IPSUM, { align="center", font_slant=CAIRO_FONT_SLANT_ITALIC, }), } -- news ticker style text line - local ticker = widget.TextLine{align="center"} + local ticker = text.TextLine{align="center"} local line_width = 80 -- arbitrary estiamte local lipsum = LOREM_IPSUM:gsub("\n", " ") lipsum = lipsum .. " " .. lipsum:sub(1, line_width) @@ -57,15 +58,15 @@ function polycore.setup() local offset = update_count % #lipsum self:set_text(lipsum:sub(offset, offset + line_width)) end - table.insert(widgets, widget.Filler()) - table.insert(widgets, widget.Frame(ticker, { + table.insert(widgets, core.Filler()) + table.insert(widgets, core.Frame(ticker, { border_sides={"top"}, border_width=1, border_color={1, 1, 1, .5}, })) - local root = widget.Frame(widget.Rows(widgets), {margin=10}) - return widget.Renderer{root=root, width=width, height=height} + local root = core.Frame(core.Rows(widgets), {margin=10}) + return core.Renderer{root=root, width=width, height=height} end diff --git a/layout.lua b/layout.lua index bc53160..96a1202 100644 --- a/layout.lua +++ b/layout.lua @@ -7,14 +7,29 @@ package.path = script_dir .. "?.lua;" .. package.path local conkyrc = require('conkyrc') local polycore = require('src/polycore') local data = require('src/data') -local widget = require('src/widget') +local core = require('src/widgets/core') +local cpu = require('src/widgets/cpu') +local drive = require('src/widgets/drive') +local gpu = require('src/widgets/gpu') +local mem = require('src/widgets/memory') +local net = require('src/widgets/network') +local text = require('src/widgets/text') + +local Frame, Filler, Rows, Columns = core.Frame, core.Filler, + core.Rows, core.Columns +local Cpu, CpuFrequencies = cpu.Cpu, cpu.CpuFrequencies +local Drive = drive.Drive +local Gpu, GpuTop = gpu.Gpu, gpu.GpuTop +local MemoryGrid = mem.MemoryGrid +local Network = net.Network +local TextLine = text.TextLine -- Draw debug information DEBUG = false --- Called once on startup to initialize widgets. --- @treturn widget.Renderer +-- @treturn core.Renderer function polycore.setup() local secondary_text_color = {.72, .72, .71, 1} -- ~b9b9b7 @@ -22,7 +37,7 @@ function polycore.setup() -- Write fan speeds. This requires lm_sensors to be installed. -- Run `sensonrs` to see if any fans are reported. If not, remove -- this section and the corresponding line below. - local fan_rpm_text = widget.TextLine{align="center", color=secondary_text_color} + local fan_rpm_text = TextLine{align="center", color=secondary_text_color} fan_rpm_text.update = function(self) local fans = data.fan_rpm() self:set_text(table.concat{fans[1], " rpm · ", fans[2], " rpm"}) @@ -30,7 +45,7 @@ function polycore.setup() -- Write individual CPU core temperatures as text. -- This also relies on lm_sensors. - local cpu_temps_text = widget.TextLine{align="center", color=secondary_text_color} + local cpu_temps_text = TextLine{align="center", color=secondary_text_color} cpu_temps_text.update = function(self) local cpu_temps = data.cpu_temperatures() self:set_text(table.concat(cpu_temps, " · ") .. " °C") @@ -38,7 +53,7 @@ function polycore.setup() -- Write individual CPU core temperatures as text. -- This also relies on lm_sensors. - local gpu_power_text = widget.TextLine{align="right", font_size=10.1} + local gpu_power_text = TextLine{align="right", font_size=10.1} gpu_power_text.update = function(self) local fans = data.fan_rpm() local gpu_power_draw = string.format("%.0f", data.gpu_power_draw()) @@ -48,39 +63,39 @@ function polycore.setup() local widgets = { fan_rpm_text, -- see above cpu_temps_text, -- see above - widget.Filler{height=3}, + Filler{height=3}, -- Adjust the CPU core count to your system. -- Requires lm_sensors for CPU temperatures. - widget.Cpu{cores=6, inner_radius=28, gap=5, outer_radius=57}, - widget.Filler{height=7}, - widget.CpuFrequencies{cores=6, min_freq=0.75, max_freq=4.3}, - widget.Filler{height=129}, + Cpu{cores=8, inner_radius=28, gap=5, outer_radius=57}, + Filler{height=7}, + CpuFrequencies{cores=8, min_freq=0.75, max_freq=4.3}, + Filler{height=129}, -- See also widget.MemoryBar - widget.MemoryGrid{rows=5}, - widget.Filler{height=78}, + MemoryGrid{rows=5}, + Filler{height=78}, -- Requires `nvidia-smi` to be installed. Does not work for AMD GPUs. gpu_power_text, -- see above - widget.Filler{height=2}, - widget.Gpu(), - widget.Filler{height=1}, - widget.GpuTop{lines=5, color=secondary_text_color}, - widget.Filler{height=66}, + Filler{height=2}, + Gpu(), + Filler{height=1}, + GpuTop{lines=5, color=secondary_text_color}, + Filler{height=66}, -- Adjust the interface name for your system. Run `ifconfig` to find -- out yours. Common names are "eth0" and "wlan0". - widget.Network{interface="enp0s31f6", downspeed=5 * 1024, upspeed=1024, + Network{interface="enp34s0u1u3u4", downspeed=5 * 1024, upspeed=1024, graph_height=22}, - widget.Filler{height=34}, + Filler{height=34}, -- Mount paths. Devices that aren't mounted will not be rendered until -- they appear. That way external drives can be displayed automatically. - widget.Drive("/"), - widget.Drive("/mnt/blackstor"), - widget.Drive("/mnt/bluestor"), - widget.Filler(), + Drive("/"), + Drive("/mnt/blackstor"), + Drive("/mnt/bluestor"), + Filler(), } local root = widget.Frame(widget.Rows(widgets), { padding={108, 9, 10, 10}, @@ -88,7 +103,7 @@ function polycore.setup() border_width = 1, border_sides = {"right"}, }) - return widget.Renderer{root=root, + return core.Renderer{root=root, width=conkyrc.config.minimum_width, height=conkyrc.config.minimum_height} end diff --git a/src/widget.lua b/src/widget.lua deleted file mode 100644 index 234380a..0000000 --- a/src/widget.lua +++ /dev/null @@ -1,1514 +0,0 @@ ---- A collection of Widget classes --- @module widget --- @alias w - -pcall(function() require('cairo') end) - -local data = require('src/data') -local util = require('src/util') -local ch = require('src/cairo_helpers') - --- lua 5.1 to 5.3 compatibility -local unpack = unpack or table.unpack -- luacheck: read_globals unpack table - -local sin, cos, tan, PI = math.sin, math.cos, math.tan, math.pi -local floor, ceil, clamp = math.floor, math.ceil, util.clamp - -local w = { - --- Font used by widgets if no other is specified. - -- @string default_font_family - default_font_family = "Ubuntu", - - --- Font size used by widgets if no other is specified. - -- @int default_font_size - default_font_size = 10, - - --- Text color used by widgets if no other is specified. - -- @tfield {number,number,number,number} default_text_color - default_text_color = {.94, .94, .94, 1}, -- ~fafafa - - --- Color used to draw some widgets if no other is specified. - -- @tfield {number,number,number,number} default_graph_color - default_graph_color = {.4, 1, 1, 1}, -} - -local temperature_colors = { - w.default_graph_color, - {.5, 1, .8}, - {.7, .9, .6}, - {1, .9, .4}, - {1, .6, .2}, - {1, .2, .2}, -} - ---- Generate a temperature based color. --- Colors are chosen based on float offset in a pre-defined color gradient. --- @number temperature current temperature (or any other type of numeric value) --- @number low threshold for lowest temperature / coolest color --- @number high threshold for highest temperature / hottest color -function w.temperature_color(temperature, low, high) - -- defaults in case temperature is nil - local cool = temperature_colors[1] - local hot = temperature_colors[1] - local weight = 0 - if type(temperature) == "number" and temperature > -math.huge and temperature < math.huge then - local idx = (temperature - low) / (high - low) * (#temperature_colors - 1) + 1 - weight = idx - floor(idx) - cool = temperature_colors[clamp(1, #temperature_colors, floor(idx))] - hot = temperature_colors[clamp(1, #temperature_colors, ceil(idx))] - end - return cool[1] + weight * (hot[1] - cool[1]), - cool[2] + weight * (hot[2] - cool[2]), - cool[3] + weight * (hot[3] - cool[3]) -end - - ---- Root widget wrapper --- Takes care of managing layout reflows and background caching. --- @type Renderer -local Renderer = util.class() -w.Renderer = Renderer - ---- --- @tparam table args table of options --- @tparam Widget args.root The Widget subclass that should be rendered, --- usually a Rows widget --- @int args.width Width of the surface that should be covered --- @int args.height Height of the surface that should be covered -function Renderer:init(args) - self._root = args.root - self._width = args.width - self._height = args.height - self._background_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, - args.width, - args.height) -end - ---- Layout all Widgets and cache their backgrounds. --- Call this once to create the initial layout. --- Will be called again automatically each time the layout changes. -function Renderer:layout() - local widgets = self._root:layout(self._width, self._height) or {} - table.insert(widgets, 1, {self._root, 0, 0, self._width, self._height}) - - local background_widgets = {} - self._update_widgets = {} - self._render_widgets = {} - for widget, x, y in util.imap(unpack, widgets) do - local matrix = cairo_matrix_t:create() - cairo_matrix_init_translate(matrix, floor(x), floor(y)) - if widget.render_background then - table.insert(background_widgets, {widget, matrix}) - end - if widget.render then - table.insert(self._render_widgets, {widget, matrix}) - end - if widget.update then - table.insert(self._update_widgets, widget) - end - end - - local cr = cairo_create(self._background_surface) - -- clear surface - cairo_save(cr) - cairo_set_source_rgba(cr, 0, 0, 0, 0) - cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE) - cairo_paint(cr) - cairo_restore(cr) - - cairo_save(cr) - for widget, matrix in util.imap(unpack, background_widgets) do - cairo_set_matrix(cr, matrix) - widget:render_background(cr) - end - cairo_restore(cr) - - if DEBUG then - local version_info = table.concat{"conky ", conky_version, - " ", _VERSION, - " cairo ", cairo_version_string()} - cairo_set_source_rgba(cr, 1, 0, 0, 1) - ch.set_font(cr, "Ubuntu", 8) - ch.write_left(cr, 0, 8, version_info) - for _, x, y, width, height in util.imap(unpack, widgets) do - if width * height ~= 0 then - cairo_rectangle(cr, x, y, width, height) - end - end - cairo_set_line_width(cr, 1) - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) - cairo_set_source_rgba(cr, 1, 0, 0, 0.33) - cairo_stroke(cr) - end - - cairo_destroy(cr) -end - ---- Update all Widgets --- @int update_count Conky's $updates -function Renderer:update(update_count) - local reflow = false - for _, widget in ipairs(self._update_widgets) do - reflow = widget:update(update_count) or reflow - end - if reflow then - self:layout() - end -end - -function Renderer:paint_background(cr) - cairo_set_source_surface(cr, self._background_surface, 0, 0) - cairo_paint(cr) -end - ---- Render to the given context --- @tparam cairo_t cr -function Renderer:render(cr) - for widget, matrix in util.imap(unpack, self._render_widgets) do - cairo_set_matrix(cr, matrix) - widget:render(cr) - end -end - - ---- Base Widget class. --- @type Widget -local Widget = util.class() -w.Widget = Widget - ---- Set a width if the Widget should have a fixed width. --- Omit (=nil) if width should be adjusted dynamically. --- @int Widget.width - ---- Set a height if the Widget should have a fixed height. --- Omit (=nil) if height should be adjusted dynamically. --- @int Widget.height - ---- Called at least once to inform the widget of the width and height --- it may occupy. --- @tparam int width --- @tparam int height -function Widget:layout(width, height) end -- luacheck: no unused - ---- Called at least once to allow the widget to draw static content. --- @function Widget:render_background --- @tparam cairo_t cr Cairo context for background rendering --- (to be cached by the `Renderer`) - ---- Called before each call to `Widget:render`. --- If this function returns a true-ish value, a reflow will be triggered. --- Since this involves calls to all widgets' :layout functions, --- reflows should be used sparingly. --- @function Widget:update --- @int update_count Conky's $updates --- @treturn ?bool true(-ish) if a layout reflow should be triggered, causing --- all `Widget:layout` and `Widget:render_background` methods --- to be called again - ---- Called once per update to do draw dynamic content. --- @function Widget:render --- @tparam cairo_t cr - - ---- Basic collection of widgets. --- Rows are drawn in a vertical stack starting at the top of the drawble --- surface. --- @type Rows -local Rows = util.class(Widget) -w.Rows = Rows - ---- @tparam {Widget,...} widgets -function Rows:init(widgets) - self._widgets = widgets - local width = 0 - self._min_height = 0 - self._fillers = 0 - for _, widget in ipairs(widgets) do - if widget.width then - if widget.width > width then width = widget.width end - end - if widget.height ~= nil then - self._min_height = self._min_height + widget.height - else - self._fillers = self._fillers + 1 - end - end - if self._fillers == 0 then - self.height = self._min_height - end -end - -function Rows:layout(width, height) - self._width = width -- used to draw debug lines - local y = 0 - local children = {} - local filler_height = (height - self._min_height) / self._fillers - for _, widget in ipairs(self._widgets) do - local widget_height = widget.height or filler_height - table.insert(children, {widget, 0, y, width, widget_height}) - local sub_children = widget:layout(width, widget_height) or {} - for _, child in ipairs(sub_children) do - child[3] = child[3] + y - table.insert(children, child) - end - y = y + widget_height - end - return children -end - - ---- Display Widgets side by side --- @type Columns -local Columns = util.class(Widget) -w.Columns = Columns - --- reuse an identical function - ---- @tparam {Widget,...} widgets -function Columns:init(widgets) - self._widgets = widgets - self._min_width = 0 - self._fillers = 0 - local height = 0 - local fix_height = false - for _, widget in ipairs(widgets) do - if widget.width ~= nil then - self._min_width = self._min_width + widget.width - else - self._fillers = self._fillers + 1 - end - if widget.height then - fix_height = true - if widget.height > height then height = widget.height end - end - end - if self._fillers == 0 then - self.width = self._min_width - end - if fix_height then - self.height = height - end -end - - -function Columns:layout(width, height) - self._height = height -- used to draw debug lines - local x = 0 - local children = {} - local filler_width = (width - self._min_width) / self._fillers - for _, widget in ipairs(self._widgets) do - local widget_width = widget.width or filler_width - table.insert(children, {widget, x, 0, widget_width, height}) - local sub_children = widget:layout(widget_width, height) or {} - for _, child in ipairs(sub_children) do - child[2] = child[2] + x - table.insert(children, child) - end - x = x + widget_width - end - return children -end - - ---- Leave space between widgets. --- If either height or width is not specified, the available space --- inside a Rows or Columns widget will be distributed evenly between Fillers --- with no fixed height/width. --- A Filler may contain one other Widget which will have its dimensions --- restricted to those of the Filler. --- @type Filler -local Filler = util.class(Widget) -w.Filler = Filler - ---- @tparam ?table args table of options --- @tparam ?int args.width --- @tparam ?int args.height --- @tparam ?Widget args.widget -function Filler:init(args) - if args then - self._widget = args.widget - self.height = args.height or (self._widget and self._widget.height) - self.width = args.width or (self._widget and self._widget.width) - end -end - -function Filler:layout(width, height) - if self._widget then - local children = self._widget:layout(width, height) or {} - table.insert(children, 1, {self._widget, 0, 0, width, height}) - return children - end -end - - -local function side_widths(arg) - arg = arg or 0 - if type(arg) == "number" then - return {top=arg, right=arg, bottom=arg, left=arg} - elseif #arg == 2 then - return {top=arg[1], right=arg[2], bottom=arg[1], left=arg[2]} - elseif #arg == 3 then - return {top=arg[1], right=arg[2], bottom=arg[3], left=arg[2]} - elseif #arg == 4 then - return {top=arg[1], right=arg[2], bottom=arg[3], left=arg[4]} - end -end - - ---- Draw a static border and/or background around/behind another widget. --- @type Frame -local Frame = util.class(Widget) -w.Frame = Frame - ---- @tparam Widget widget Widget to be wrapped --- @tparam table args table of options --- @tparam ?number|{number,...} args.padding Leave some space around the inside --- of the frame.
    --- - number: same padding all around.
    --- - table of two numbers: {top & bottom, left & right}
    --- - table of three numbers: {top, left & right, bottom}
    --- - table of four numbers: {top, right, bottom, left} --- @tparam ?number|{number,...} args.margin Like padding but outside the border. --- @tparam ?{number,number,number,number} args.background_color --- @tparam[opt=transparent] ?{number,number,number,number} args.border_color --- @tparam[opt=0] ?number args.border_width border line width --- @tparam ?{string,...} args.border_sides any combination of --- "top", "right", "bottom" and/or "left" --- (default: all sides) -function Frame:init(widget, args) - self._widget = widget - self._background_color = args.background_color or nil - self._border_color = args.border_color or {0, 0, 0, 0} - self._border_width = args.border_width or 0 - - self._padding = side_widths(args.padding) - self._margin = side_widths(args.margin) - self._border_sides = util.set(args.border_sides or {"top", "right", "bottom", "left"}) - - self._has_background = self._background_color and self._background_color[4] > 0 - self._has_border = self._border_width > 0 - and (not args.border_sides or #args.border_sides > 0) - - self._x_left = self._margin.left + self._padding.left - + (self._border_sides.left and self._border_width or 0) - self._y_top = self._margin.top + self._padding.top - + (self._border_sides.top and self._border_width or 0) - self._x_right = self._margin.right + self._padding.right - + (self._border_sides.right and self._border_width or 0) - self._y_bottom = self._margin.bottom + self._padding.bottom - + (self._border_sides.bottom and self._border_width or 0) - - if widget.width then - self.width = widget.width + self._x_left + self._x_right - end - if widget.height then - self.height = widget.height + self._y_top + self._y_bottom - end -end - -function Frame:layout(width, height) - self._width = width - self._margin.left - self._margin.right - self._height = height - self._margin.top - self._margin.bottom - local inner_width = width - self._x_left - self._x_right - local inner_height = height - self._y_top - self._y_bottom - local children = self._widget:layout(inner_width, inner_height) or {} - for _, child in ipairs(children) do - child[2] = child[2] + self._x_left - child[3] = child[3] + self._y_top - end - table.insert(children, 1, {self._widget, self._x_left, self._y_top, inner_width, inner_height}) - return children -end - -function Frame:render_background(cr) - if self._has_background then - cairo_rectangle(cr, self._margin.left, self._margin.top, self._width, self._height) - cairo_set_source_rgba(cr, unpack(self._background_color)) - cairo_fill(cr) - end - - if self._has_border then - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) - cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE) - cairo_set_source_rgba(cr, unpack(self._border_color)) - cairo_set_line_width(cr, self._border_width) - local x_min = self._margin.left + 0.5 * self._border_width - local y_min = self._margin.top + 0.5 * self._border_width - local x_max = self._margin.left + self._width - 0.5 * self._border_width - local y_max = self._margin.top + self._height - 0.5 * self._border_width - local side, line, move = self._border_sides, cairo_line_to, cairo_move_to - cairo_move_to(cr, x_min, y_min); - (side.top and line or move)(cr, x_max, y_min); - (side.right and line or move)(cr, x_max, y_max); - (side.bottom and line or move)(cr, x_min, y_max); - (side.left and line or move)(cr, x_min, y_min); - cairo_stroke(cr, self._background_color) - end -end - ---- Common (abstract) base class for `StaticText` and `TextLine`. --- @type Text -local Text = util.class(Widget) -w.Text = Text - -local write_aligned = {left = ch.write_left, - center = ch.write_centered, - right = ch.write_right} - ---- @tparam table args table of options --- @tparam ?string args.align "left" (default), "center" or "right" --- @tparam[opt=w.default_font_family] ?string args.font_family --- @tparam[opt=w.default_font_size] ?number args.font_size --- @tparam[opt=CAIRO_FONT_SLANT_NORMAL] ?cairo_font_slant_t args.font_slant --- @tparam[opt=CAIRO_FONT_WEIGHT_NORMAL] ?cairo_font_weight_t args.font_weight --- @tparam ?{number,number,number,number} args.color (default: `default_text_color`) -function Text:init(args) - assert(getmetatable(self) ~= Text, "Cannot instanciate class Text directly.") - self._align = args.align or "left" - self._font_family = args.font_family or w.default_font_family - self._font_size = args.font_size or w.default_font_size - self._font_slant = args.font_slant or CAIRO_FONT_SLANT_NORMAL - self._font_weight = args.font_weight or CAIRO_FONT_WEIGHT_NORMAL - self._color = args.color or w.default_text_color - - self._write_fn = write_aligned[self._align] - - -- try to match conky's line spacing: - local font_extents = ch.font_extents(self._font_family, self._font_size, - self._font_slant, self._font_weight) - self._line_height = font_extents.height + 1 - - local line_spacing = font_extents.height - (font_extents.ascent + font_extents.descent) - self._baseline_offset = font_extents.ascent + 0.5 * line_spacing + 1 -end - -function Text:layout(width) - if self._align == "center" then - self._x = 0.5 * width - elseif self._align == "left" then - self._x = 0 - else -- self._align == "right" - self._x = width - end -end - - ---- Draw some unchangeable text. --- Use this widget for text that will never be updated. --- @type StaticText -local StaticText = util.class(Text) -w.StaticText = StaticText - ---- @string text Text to be displayed. --- @tparam ?table args table of options, see `Text:init` -function StaticText:init(text, args) - Text.init(self, args or {}) - - self._lines = {} - text = text .. "\n" - - for line in text:gmatch("(.-)\n") do - table.insert(self._lines, line) - end - - self.height = #self._lines * self._line_height -end - -function StaticText:render_background(cr) - ch.set_font(cr, self._font_family, self._font_size, self._font_slant, - self._font_weight) - cairo_set_source_rgba(cr, unpack(self._color)) - for i, line in ipairs(self._lines) do - local y = self._baseline_offset + (i - 1) * self._line_height - self._write_fn(cr, self._x, y, line) - end -end - - ---- Draw a single line of changeable text. --- Text line can be updated on each cycle via `set_text`. --- @type TextLine -local TextLine = util.class(Text) -w.TextLine = TextLine - ---- @tparam table args table of options, see `Text:init` -function TextLine:init(args) - Text.init(self, args) - self.height = self._line_height -end - ---- Update the text line to be displayed. --- @string text -function TextLine:set_text(text) - self._text = text -end - -function TextLine:render(cr) - ch.set_font(cr, self._font_family, self._font_size, self._font_slant, - self._font_weight) - cairo_set_source_rgba(cr, unpack(self._color)) - self._write_fn(cr, self._x, self._baseline_offset, self._text) -end - - ---- Progress-bar like box, similar to conky's bar. --- Can have small and big ticks for visual clarity, --- and a unit (static, up to 3 characters) written behind the end. --- @type Bar -local Bar = util.class(Widget) -w.Bar = Bar - ---- @tparam table args table of options --- @tparam[opt=6] int args.thickness vertical size of the bar --- @tparam ?string args.unit to be drawn behind the bar - 3 characters will fit --- @tparam ?{number,...} args.ticks relative offsets (between 0 and 1) of ticks --- @tparam ?int args.big_ticks multiple of ticks to be drawn longer --- @tparam ?{number,number,number} args.color (default: `default_graph_color`) -function Bar:init(args) - self._ticks = args.ticks - self._big_ticks = args.big_ticks - self._unit = args.unit - self._thickness = (args.thickness or 4) - self.height = self._thickness + 2 - self.color = args.color or w.default_graph_color - - if self._ticks then - self.height = self.height + (self._big_ticks and 3 or 2) - end - if self._unit then - self.height = math.max(self.height, 8) -- line_height - end - - self._fraction = 0 -end - -function Bar:layout(width) - self._width = width - (self._unit and 20 or 0) - 2 - self._tick_coordinates = {} - if self._ticks then - local x, tick_length - for i, frac in ipairs(self._ticks) do - x = math.floor(frac * self._width) - tick_length = 3 - if self._big_ticks then - if i % self._big_ticks == 0 then - tick_length = 3 - else - tick_length = 2 - end - end - table.insert(self._tick_coordinates, {x, self._thickness + 1, tick_length}) - end - end -end - -function Bar:render_background(cr) - if self._unit then - cairo_set_source_rgba(cr, unpack(w.default_text_color)) - ch.set_font(cr, w.default_font_family, w.default_font_size) - ch.write_left(cr, self._width + 5, 6, self._unit) - end - -- fake shadow border - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) - cairo_set_line_width(cr, 1) - cairo_rectangle(cr, 0, 0, self._width + 1, self._thickness + 1) - cairo_set_source_rgba(cr, 0, 0, 0, .66) - cairo_stroke(cr) -end - ---- Set the fill-ratio of the bar --- @number fraction between 0 and 1 -function Bar:set_fill(fraction) - self._fraction = fraction -end - -function Bar:render(cr) - local r, g, b = unpack(self.color) - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) - cairo_set_line_width(cr, 1) - - cairo_rectangle(cr, 0, 0, self._width, self._thickness) - ch.alpha_gradient(cr, 0, 0, self._width, 0, r, g, b, { - self._fraction - 0.33, 0.33, - self._fraction - 0.08, 0.66, - self._fraction - 0.01, 0.75, - self._fraction, 1, - self._fraction + 0.01, 0.2, - self._fraction + 0.1, 0.1, - 1, 0.15, - }) - cairo_fill(cr) - - -- border - cairo_rectangle(cr, 1, 1, self._width - 1, self._thickness - 1) - cairo_set_source_rgba(cr, r, g, b, .2) - cairo_stroke(cr) - - -- ticks - for _, tick in ipairs(self._tick_coordinates) do - cairo_move_to(cr, tick[1], tick[2]) - cairo_rel_line_to(cr, 0, tick[3]) - end - cairo_set_source_rgba(cr, r, g, b, .5) - cairo_stroke(cr) -end - - ---- Specialized unit-based Bar. --- @type MemoryBar -local MemoryBar = util.class(Bar) -w.MemoryBar = MemoryBar - ---- @tparam table args table of options --- @tparam ?number args.total Total amount of memory to be represented --- by this bar. If greater than 8, ticks will be --- drawn. If omitted, total RAM will be used, --- however no ticks can be drawn. --- @tparam[opt="GiB"] string args.unit passed to `Bar:init` --- @tparam ?int args.thickness passed to `Bar:init` --- @tparam ?{number,number,number} args.color passed to `Bar:init` -function MemoryBar:init(args) - self._total = args.total - local ticks, big_ticks - if self._total then - local max_tick = floor(self._total) - ticks = util.range(1 / self._total, max_tick / self._total, 1 / self._total) - big_ticks = max_tick > 8 and 4 or nil - end - Bar.init(self, {ticks=ticks, - big_ticks=big_ticks, - unit=args.unit or "GiB", - thickness=args.thickness, - color=args.color}) -end - ---- Set the amount of used memory as an absolute value. --- @number used should be between 0 and args.total -function MemoryBar:set_used(used) - self:set_fill(used / self._total) -end - -function MemoryBar:update() - local used, _, _, total = data.memory("GiB") - self:set_fill(used / total) -end - - ---- Track changing data; similar to conky's graphs. --- @type Graph -local Graph = util.class(Widget) -w.Graph = Graph - ---- @tparam table args table of options --- @tparam number args.max maximum expected value to be represented; --- may be expanded automatically as need arises --- @int[opt=60] args.data_points how many values to store --- @bool[opt=false] args.upside_down Draw graph from top to bottom? --- @number[opt=0.5] args.smoothness Bézier curves smoothness. --- Set to 0 to draw straight lines instead, --- which may be slightly faster. --- @int[opt] args.width fix width in pixels --- @int[opt] args.height fixeheight in pixels --- @tparam ?{number,number,number} args.color (default: `default_graph_color`) -function Graph:init(args) - self._max = args.max - self._data = util.CycleQueue(args.data_points or 60) - self._upside_down = args.upside_down or false - self._smoothness = args.smoothness or 0.5 - self.color = args.color or w.default_graph_color - self.width = args.width - self.height = args.height -end - -function Graph:layout(width, height) - self._width = width - 2 - self._height = height - 2 - self._x_scale = (width - 2) / (self._data.length - 1) - self._y_scale = (height - 3) / self._max - if self._upside_down then - self._y_scale = -self._y_scale - self._y = -0.5 - else - self._y = self._height - 0.5 - end -end - -function Graph:render_background(cr) - local r, g, b = unpack(self.color) - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) - - -- background - cairo_rectangle(cr, 0, 0, self._width + 1, self._height + 1) - ch.alpha_gradient(cr, 0, 0, 0, self._height, r, g, b, {0, .15, 1, .03}) - cairo_fill(cr) - - -- grid - cairo_set_line_width(cr, 1) - cairo_set_source_rgba(cr, r, g, b, .0667) - cairo_rectangle(cr, 1, 1, self._width - 1, self._height - 1) - local gridsize = 5 - for row = gridsize + 1, self._height, gridsize do - cairo_move_to(cr, 1, row) - cairo_line_to(cr, self._width, row) - end - for column = gridsize + 1, self._width, gridsize do - cairo_move_to(cr, column, 1) - cairo_line_to(cr, column, self._height) - end - cairo_stroke(cr) - for row = gridsize + 1, self._height, gridsize do - for column = gridsize + 1, self._width, gridsize do - cairo_rectangle(cr, column - 0.5, row - 0.5, 0.5, 0.5) - end - end - cairo_set_source_rgba(cr, r, g, b, .15) - cairo_stroke(cr) -end - ---- Append the latest value to be shown - this will displace the oldest value --- @number value if value > args.max then the graphs vertical scale will be --- adjusted, causing it to get squished -function Graph:add_value(value) - self._data:put(value) - if value > self._max then - self._max = value - self:layout(self._width + 2, self._height + 2) - end -end - -function Graph:_line_path(cr) - local current_max = 0 - cairo_move_to(cr, 0.5, self._y - self._data[1] * self._y_scale) - for idx, val in self._data:__ipairs() do - if current_max < val then current_max = val end - if idx > 1 then - cairo_line_to(cr, 0.5 + (idx - 1) * self._x_scale, - self._y - val * self._y_scale) - end - end - return current_max -end - -function Graph:_berzier_path(cr) - local current_max = 0 - local prev_x, prev_y = 0.5, self._y - self._data[1] * self._y_scale - cairo_move_to(cr, prev_x, prev_y) - for idx, val in self._data:__ipairs() do - if current_max < val then current_max = val end - if idx > 1 then - local current_x = 0.5 + (idx - 1) * self._x_scale - local current_y = self._y - val * self._y_scale - local x1 = prev_x + self._smoothness * self._x_scale - local x2 = current_x - self._smoothness * self._x_scale - cairo_curve_to(cr, x1, prev_y, x2, current_y, current_x, current_y) - prev_x, prev_y = current_x, current_y - end - end - return current_max -end - -function Graph:render(cr) - local r, g, b = unpack(self.color) - cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT) - cairo_set_source_rgba(cr, r, g, b, 1) - cairo_set_line_width(cr, 0.5) - - local current_max = self._smoothness > 0 and self:_berzier_path(cr) - or self:_line_path(cr) - - if current_max > 0 then -- fill under graph - cairo_stroke_preserve(cr) - cairo_line_to(cr, self._width, self._y) - cairo_line_to(cr, 0, self._y) - cairo_close_path(cr) - ch.alpha_gradient(cr, 0, self._y - current_max * self._y_scale, 0, self._y, - r, g, b, {0, .66, .5, .33, 1, .25}) - cairo_fill(cr) - else - cairo_stroke(cr) - end -end - - ---- Round light indicator for minimalistic feedback. --- @type LED -local LED = util.class(Widget) -w.LED = LED - ---- @tparam table args table of options --- @number args.radius size of the LED --- @number[opt=0] args.brightness between 0 and 1, how "on" should the LED be? --- Can be changed later with `LED:set_brightness` --- @tparam ?{number,number,number} args.color color of the LED, --- can be changed later with `LED:set_color`. --- (default: `default_graph_color`) --- @tparam ?{number,number,number,number} args.background_color mostly visible --- when the LED is off. This allows you to choose --- a neutral background if you plan on changing --- the light color via `LED:set_color`. --- (default: darkened `args.color`) -function LED:init(args) - assert(args.radius) - self._radius = args.radius - self.width = self._radius * 2 - self.height = self._radius * 2 - self._brightness = args.brightness or 0 - self._color = args.color or w.default_graph_color - if args.background_color then - self._background_color = args.background_color - else - local r, g, b = unpack(self._color) - self._background_color = {0.2 * r, 0.2 * g, 0.2 * b, 0.75} - end -end - ---- @number brightness between 0 and 1 -function LED:set_brightness(brightness) - self._brightness = clamp(0, 1, brightness) -end - ---- @tparam ?{number,number,number} color -function LED:set_color(color) - self._color = color -end - -function LED:layout(width, height) - self._mx = width / 2 - self._my = height / 2 -end - -function LED:render_background(cr) - cairo_arc(cr, self._mx, self._my, self._radius, 0, 360) - cairo_set_source_rgba(cr, unpack(self._background_color)) - cairo_fill(cr) -end - -function LED:render(cr) - if self._brightness > 0 then - local r, g, b = unpack(self._color) - local gradient = cairo_pattern_create_radial(self._mx, self._my, 0, - self._mx, self._my, self._radius) - cairo_pattern_add_color_stop_rgba(gradient, 0, r, g, b, 1 * self._brightness) - cairo_pattern_add_color_stop_rgba(gradient, 0.5, r, g, b, 0.5 * self._brightness) - cairo_pattern_add_color_stop_rgba(gradient, 1, r, g, b, 0.1 * self._brightness) - cairo_set_source(cr, gradient) - cairo_pattern_destroy(gradient) - - cairo_arc(cr, self._mx, self._my, self._radius, 0, 360) - cairo_fill(cr) - end -end - - ---- Polygon-style CPU usage & temperature indicator. --- Looks best for CPUs with 4 to 8 cores but also works for higher numbers. --- @type Cpu -local Cpu = util.class(Widget) -w.Cpu = Cpu - ---- @tparam table args table of options --- @int args.cores How many cores does your CPU have? --- @int args.scale radius of central polygon --- @int args.gap space between central polygon and outer segments --- @int args.segment_size radial thickness of outer segments -function Cpu:init(args) - self._cores = args.cores - self._inner_radius = args.inner_radius - self._outer_radius = args.outer_radius - self._gap = args.gap or 4 - - if self._outer_radius then - self.height = 2 * self._outer_radius - self.width = self.height - end -end - -function Cpu:layout(width, height) - if not self._outer_radius then - self._outer_radius = 0.5 * math.min(width, height) - end - if not self._inner_radius then - self._inner_radius = 0.5 * self._outer_radius - end - self._mx = width / 2 - self._my = height / 2 - - self._center_coordinates = {} - self._segment_coordinates = {} - self._gradient_coordinates = {} - local sector_rad = 2 * PI / self._cores - local center_scale = self._inner_radius - 2.5 -- thick stroke - local min = self._inner_radius - local max = self._outer_radius - self._gap - for core = 1, self._cores do - local rad_center = (core - 1) * sector_rad - PI / 2 - local rad_left = rad_center + sector_rad / 2 - local rad_right = rad_center - sector_rad / 2 - local dx_center, dy_center = cos(rad_center), sin(rad_center) - local dx_left, dy_left = cos(rad_left), sin(rad_left) - local dx_right, dy_right = cos(rad_right), sin(rad_right) - self._center_coordinates[2 * core - 1] = self._mx + center_scale * dx_left - self._center_coordinates[2 * core] = self._my + center_scale * dy_left - - -- segment corners - local dx_gap, dy_gap = self._gap * dx_center, self._gap * dy_center - local x1 = self._mx + min * dx_left + dx_gap - local y1 = self._my + min * dy_left + dy_gap - local x2 = self._mx + max * dx_left + dx_gap - local y2 = self._my + max * dy_left + dy_gap - local x3 = self._mx + max * dx_right + dx_gap - local y3 = self._my + max * dy_right + dy_gap - local x4 = self._mx + min * dx_right + dx_gap - local y4 = self._my + min * dy_right + dy_gap - self._segment_coordinates[core] = {x1, y1, x2, y2, x3, y3, x4, y4} - self._gradient_coordinates[core] = {(x1 + x4) / 2, (y1 + y4) / 2, - (x2 + x3) / 2, (y2 + y3) / 2} - end -end - -function Cpu:update() - self._percentages = data.cpu_percentages(self._cores) - self._temperatures = data.cpu_temperatures() -end - -function Cpu:render(cr) - local avg_temperature = util.avg(self._temperatures) - local r, g, b = w.temperature_color(avg_temperature, 30, 80) - - ch.polygon(cr, self._center_coordinates) - cairo_set_line_width(cr, 6) - cairo_set_source_rgba(cr, r, g, b, .33) - cairo_stroke_preserve(cr) - cairo_set_line_width(cr, 1) - cairo_set_source_rgba(cr, 0, 0, 0, .66) - cairo_stroke_preserve(cr) - cairo_set_source_rgba(cr, r, g, b, .18) - cairo_fill(cr) - - cairo_set_source_rgba(cr, r, g, b, .4) - ch.set_font(cr, w.default_font_family, 16, nil, CAIRO_FONT_WEIGHT_BOLD) - ch.write_middle(cr, self._mx + 1, self._my, string.format("%.0f°", avg_temperature)) - - for core = 1, self._cores do - ch.polygon(cr, self._segment_coordinates[core]) - local gradient = cairo_pattern_create_linear(unpack(self._gradient_coordinates[core])) - r, g, b = w.temperature_color(self._temperatures[core], 30, 80) - cairo_set_source_rgba(cr, 0, 0, 0, .4) - cairo_set_line_width(cr, 1.5) - cairo_stroke_preserve(cr) - cairo_set_source_rgba(cr, r, g, b, .4) - cairo_set_line_width(cr, .75) - cairo_stroke_preserve(cr) - - local h_rel = self._percentages[core]/100 - cairo_pattern_add_color_stop_rgba(gradient, 0, r, g, b, .33) - cairo_pattern_add_color_stop_rgba(gradient, h_rel - .045, r, g, b, .75) - cairo_pattern_add_color_stop_rgba(gradient, h_rel, - r * 1.2, g * 1.2, b * 1.2, 1) - if h_rel < .95 then -- prevent pixelated edge - cairo_pattern_add_color_stop_rgba(gradient, h_rel + .045, r, g, b, .33) - cairo_pattern_add_color_stop_rgba(gradient, h_rel + .33, r, g, b, .15) - cairo_pattern_add_color_stop_rgba(gradient, 1, r, g, b, .15) - end - cairo_set_source(cr, gradient) - cairo_pattern_destroy(gradient) - cairo_fill(cr) - end -end - - ---- Round CPU usage & temperature indicator. --- Best suited for CPUs with high core counts. --- @type CpuRound -local CpuRound = util.class(Widget) -w.CpuRound = CpuRound - -CpuRound.update = Cpu.update - ---- @tparam table args table of options --- @int args.cores How many cores does your CPU have? --- @int args.inner_radius Size of inner circle --- @int args.outer_radius Max radius for core at 100% --- @int[opt] args.grid Number of grid lines to draw in the background. -function CpuRound:init(args) - self._cores = args.cores - self._inner_radius = args.inner_radius - self._outer_radius = args.outer_radius - self._grid = args.grid - - if self._outer_radius then - self.height = 2 * self._outer_radius - self.width = self.height - end -end - -function CpuRound:layout(width, height) - if not self._outer_radius then - self._outer_radius = 0.5 * math.min(width, height) - end - if not self._inner_radius then - self._inner_radius = 0.75 * self._outer_radius - end - self._center = {width / 2, height / 2} - local sector_rad = 2 * PI / self._cores - - -- choose control points that best approximate a circle, see - -- https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves - local ctrl_length = 1.3333 * tan(0.25 * sector_rad) - self._points = {} - for core = 1, self._cores do - local rad = (core - 1) * sector_rad - local dx, dy = cos(rad), sin(rad) - self._points[core] = { - dx = dx, - dy = dy, - ctrl_left_dx = dx - dy * ctrl_length, - ctrl_left_dy = dy + dx * ctrl_length, - ctrl_right_dx = dx + dy * ctrl_length, - ctrl_right_dy = dy - dx * ctrl_length, - } - end - self._points[self._cores + 1] = self._points[1] -- easy cycling -end - -function CpuRound:render_background(cr) - if not self._grid or self._grid < 1 then - return - end - local mx, my = unpack(self._center) - local gap = (self._outer_radius - self._inner_radius) / self._grid - for line = 0, self._grid do - local scale = self._inner_radius + gap * line - cairo_move_to(cr, mx + self._points[1].dx * scale, - my + self._points[1].dy * scale) - cairo_arc(cr, mx, my, scale, 0, 2 * PI) - end - for _, point in ipairs(self._points) do - cairo_move_to(cr, mx + point.dx * self._inner_radius, - my + point.dy * self._inner_radius) - cairo_line_to(cr, mx + point.dx * self._outer_radius, - my + point.dy * self._outer_radius) - end - local r, g, b = unpack(w.default_graph_color) - cairo_set_source_rgba(cr, r, g, b, 0.2) - cairo_set_line_width(cr, 1) - cairo_stroke(cr) -end - -function CpuRound:render(cr) - local avg_temperature = util.avg(self._temperatures) - local avg_percentage = util.avg(self._percentages) - local r, g, b = w.temperature_color(avg_temperature, 30, 80) - local mx, my = unpack(self._center) - - -- glow - ch.alpha_gradient_radial(cr, mx, my, self._inner_radius, - mx, my, - self._outer_radius * (1 + 0.5 * avg_percentage / 100), - r, g, b, {0, 0, 0.05, 0.2, 1, 0}) - cairo_paint(cr) - - -- temperature text - cairo_set_source_rgba(cr, r, g, b, 0.5) - ch.set_font(cr, w.default_font_family, 16, nil, CAIRO_FONT_WEIGHT_BOLD) - ch.write_middle(cr, mx + 1, my, string.format("%.0f°", avg_temperature)) - - -- inner fill - cairo_new_path(cr) - cairo_arc(cr, mx, my, self._inner_radius * 0.99, 0, 2 * PI) - ch.alpha_gradient_radial(cr, mx - self._inner_radius * 0.5, - my - self._inner_radius * 0.5, - 0, - mx, my, self._inner_radius, - r, g, b, {0, 0.4, 0.66, 0.15, 1, 0.1}) - cairo_fill(cr) - - -- usage curve - local dr = self._outer_radius - self._inner_radius - for core = 1, self._cores do - local point = self._points[core] - local scale = self._inner_radius + dr * self._percentages[core] / 100 - point.x = mx + point.dx * scale - point.y = my + point.dy * scale - point.ctrl_left_x = mx + point.ctrl_left_dx * scale - point.ctrl_left_y = my + point.ctrl_left_dy * scale - point.ctrl_right_x = mx + point.ctrl_right_dx * scale - point.ctrl_right_y = my + point.ctrl_right_dy * scale - end - cairo_move_to(cr, self._points[1].x, self._points[1].y) - for core = 1, self._cores do - local current, next = self._points[core], self._points[core + 1] - cairo_curve_to(cr, current.ctrl_left_x, - current.ctrl_left_y, - next.ctrl_right_x, - next.ctrl_right_y, - next.x, - next.y) - end - cairo_close_path(cr) - - ch.alpha_gradient_radial(cr, mx, my, self._inner_radius, - mx, my, self._outer_radius, - r, g, b, {0, 0, 0.05, 0.4, 1, 0.9}) - cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT) - cairo_fill_preserve(cr) - cairo_set_source_rgba(cr, r, g, b, 1) - cairo_set_line_width(cr, 0.75) - cairo_stroke(cr) -end - - ---- Visualize cpu-frequencies in a style reminiscent of stacked progress bars. --- @type CpuFrequencies -local CpuFrequencies = util.class(Widget) -w.CpuFrequencies = CpuFrequencies - ---- @tparam table args table of options --- @int args.cores How many cores does your CPU have? --- @number args.min_freq What is your CPU's minimum frequency? --- @number args.min_freq What is your CPU's maximum frequency? --- @int[opt=16] args.height Maximum pixel height of the drawn shape -function CpuFrequencies:init(args) - self.cores = args.cores - self.min_freq = args.min_freq - self.max_freq = args.max_freq - self._height = args.height or 16 - self.height = self._height + 13 -end - -function CpuFrequencies:layout(width) - self._width = width - 25 - self._polygon_coordinates = { - 0, self._height * (1 - self.min_freq / self.max_freq), - self._width, 0, - self._width, self._height, - 0, self._height, - } - self._ticks = {} - self._tick_labels = {} - - local df = self.max_freq - self.min_freq - for freq = 1, self.max_freq, .25 do - local x = self._width * (freq - self.min_freq) / df - local big = math.floor(freq) == freq - if big then - table.insert(self._tick_labels, {x, self._height + 11.5, ("%.0f"):format(freq)}) - end - table.insert(self._ticks, {math.floor(x) + .5, - self._height + 2, - big and 3 or 2}) - end -end - -function CpuFrequencies:render_background(cr) - cairo_set_source_rgba(cr, unpack(w.default_text_color)) - ch.set_font(cr, w.default_font_family, w.default_font_size) - ch.write_left(cr, self._width + 5, 0.5 * self._height + 3, "GHz") - - -- shadow outline - ch.polygon(cr, { - self._polygon_coordinates[1] - 1, self._polygon_coordinates[2] - 1, - self._polygon_coordinates[3] + 1, self._polygon_coordinates[4] - 1, - self._polygon_coordinates[5] + 1, self._polygon_coordinates[6] + 1, - self._polygon_coordinates[7] - 1, self._polygon_coordinates[8] + 1, - }) - cairo_set_source_rgba(cr, 0, 0, 0, .4) - cairo_set_line_width(cr, 1) - cairo_stroke(cr) -end - -function CpuFrequencies:update() - self.frequencies = data.cpu_frequencies(self.cores) - self.temperatures = data.cpu_temperatures() -end - -function CpuFrequencies:render(cr) - local r, g, b = w.temperature_color(util.avg(self.temperatures), 30, 80) - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) - cairo_set_line_width(cr, 1) - - -- ticks -- - cairo_set_source_rgba(cr, r, g, b, .66) - for _, tick in ipairs(self._ticks) do - cairo_move_to(cr, tick[1], tick[2]) - cairo_rel_line_to(cr, 0, tick[3]) - end - cairo_stroke(cr) - ch.set_font(cr, w.default_font_family, w.default_font_size) - for _, label in ipairs(self._tick_labels) do - ch.write_centered(cr, label[1], label[2], label[3]) - end - - -- background -- - ch.polygon(cr, self._polygon_coordinates) - cairo_set_source_rgba(cr, r, g, b, .15) - cairo_fill_preserve(cr) - cairo_set_source_rgba(cr, r, g, b, .3) - cairo_stroke_preserve(cr) - - -- frequencies -- - local df = self.max_freq - self.min_freq - for _, frequency in ipairs(self.frequencies) do - local stop = (frequency - self.min_freq) / df - ch.alpha_gradient(cr, 0, 0, self._width, 0, r, g, b, { - 0, 0.01, - stop - .4, 0.015, - stop - .2, 0.05, - stop - .1, 0.1, - stop - .02, 0.2, - stop, 0.6, - stop, 0, - }) - cairo_fill_preserve(cr) - end - cairo_new_path(cr) -end - - ---- Visualize memory usage in a randomized grid. --- Does not represent actual distribution of used memory. --- Also shows buffere/cache memory at reduced brightness. --- @type MemoryGrid -local MemoryGrid = util.class(Widget) -w.MemoryGrid = MemoryGrid - ---- @tparam table args table of options --- @tparam ?int args.rows Number of rows to draw. --- For nil it will be determined based on Widget height. --- @tparam ?int args.columns Number of columns to draw. --- For nil it will be determined based on Widget width. --- @tparam[opt=2] ?int args.point_size edge length of individual squares --- @tparam[opt=1] ?int args.gap space between squares --- @tparam[opt=true] ?bool args.shuffle randomize? --- @tparam ?{number,number,number} args.color (default: `default_graph_color`) -function MemoryGrid:init(args) - self._rows = args.rows - self._columns = args.columns - self._point_size = args.point_size or 2 - self._gap = args.gap or 1 - self._shuffle = args.shuffle == nil and true or args.shuffle - self._color = args.color or w.default_graph_color - if self._rows then - self.height = self._rows * self._point_size + (self._rows - 1) * self._gap - end - if self._columns then - self.width = self._columns * self._point_size + (self._columns - 1) * self._gap - end -end - -function MemoryGrid:layout(width, height) - local point_plus_gap = self._point_size + self._gap - local columns = self._columns or math.floor(width / point_plus_gap) - local rows = self._rows or math.floor(height / point_plus_gap) - local left = 0.5 * (width - columns * point_plus_gap + self._gap) - self._coordinates = {} - for col = 0, columns - 1 do - for row = 0, rows - 1 do - table.insert(self._coordinates, {col * point_plus_gap + left, - row * point_plus_gap, - self._point_size, self._point_size}) - end - end - if self._shuffle == nil or self._shuffle then - util.shuffle(self._coordinates) - end -end - -function MemoryGrid:update() - self._used, self._easyfree, self._free, self._total = data.memory("GiB") -end - -function MemoryGrid:render(cr) - if self._total <= 0 then return end -- TODO figure out why this happens - local total_points = #self._coordinates - local used_points = math.floor(total_points * self._used / self._total + 0.5) - local free_points = math.floor(total_points * self._free / self._total + 0.5) - local r, g, b = unpack(self._color) - - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) - for i = 1, used_points do - cairo_rectangle(cr, unpack(self._coordinates[i])) - end - cairo_set_source_rgba(cr, r, g, b, .8) - cairo_fill(cr) - for i = used_points, total_points - free_points do - cairo_rectangle(cr, unpack(self._coordinates[i])) - end - cairo_set_source_rgba(cr, r, g, b, .35) - cairo_fill(cr) - for i = total_points - free_points, total_points do - cairo_rectangle(cr, unpack(self._coordinates[i])) - end - cairo_set_source_rgba(cr, r, g, b, .1) - cairo_fill(cr) -end - - ---- Compound widget to display GPU and VRAM usage. --- @type Gpu -local Gpu = util.class(Rows) -w.Gpu = Gpu - ---- no options -function Gpu:init() - self._usebar = Bar{ticks={.25, .5, .75}, unit="%"} - - local _, mem_total = data.gpu_memory() - self._membar = MemoryBar{total=mem_total / 1024} - self._membar.update = function() - self._membar:set_used(data.gpu_memory() / 1024) - end - Rows.init(self, {self._usebar, Filler{height=4}, self._membar}) -end - -function Gpu:update() - self._usebar:set_fill(data.gpu_percentage() / 100) - - local color = {w.temperature_color(data.gpu_temperature(), 30, 80)} - self._usebar.color = color - self._membar.color = color -end - ---- Table of processes for the GPU, sorted by VRAM usage --- @type GpuTop -local GpuTop = util.class(Widget) -w.GpuTop = GpuTop - ---- @tparam table args table of options --- @tparam[opt=5] ?int args.lines how many processes to display --- @tparam ?string args.font_family --- @tparam ?number args.font_size --- @tparam ?{number,number,number} args.color (default: `default_text_color`) -function GpuTop:init(args) - self._lines = args.lines or 5 - self._font_family = args.font_family or w.default_font_family - self._font_size = args.font_size or w.default_font_size - self._color = args.color or w.default_text_color - - local extents = ch.font_extents(self._font_family, self._font_size) - self._line_height = extents.height - self.height = self._lines * self._line_height - local line_spacing = extents.height - (extents.ascent + extents.descent) - -- try to match conky's line spacing: - self._baseline_offset = extents.ascent + 0.5 * line_spacing + 1 -end - -function GpuTop:layout(width) - self._width = width -end - -function GpuTop:update() - self._processes = data.gpu_top() -end - -function GpuTop:render(cr) - ch.set_font(cr, self._font_family, self._font_size) - cairo_set_source_rgba(cr, unpack(self._color)) - cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) - - local lines = math.min(self._lines, #self._processes) - local y = self._baseline_offset - for i = 1, lines do - ch.write_left(cr, 0, y, self._processes[i][1]) - ch.write_right(cr, self._width, y, self._processes[i][2] .. " MiB") - y = y + self._line_height - end -end - ---- Graphs for up- and download speed. --- This widget assumes that your conky.text adds some text between the graphs. --- @type Network -local Network = util.class(Rows) -w.Network = Network - ---- @tparam table args table of options --- @string args.interface e.g. "eth0" --- @tparam ?int args.graph_height passed to `Graph:init` --- @number[opt=1024] args.downspeed passed as args.max to download speed graph --- @number[opt=1024] args.upspeed passed as args.max to upload speed graph -function Network:init(args) - self.interface = args.interface - self._downspeed_graph = Graph{height=args.graph_height, max=args.downspeed or 1024} - self._upspeed_graph = Graph{height=args.graph_height, max=args.upspeed or 1024} - Rows.init(self, {self._downspeed_graph, Filler{height=31}, self._upspeed_graph}) -end - -function Network:update() - local down, up = data.network_speed(self.interface) - self._downspeed_graph:add_value(down) - self._upspeed_graph:add_value(up) -end - ---- Visualize drive usage and temperature in a colorized Bar. --- Also writes temperature as text. --- This widget is exptected to be combined with some special conky.text. --- @type Drive -local Drive = util.class(Rows) -w.Drive = Drive - ---- @string path e.g. "/home" -function Drive:init(path) - self._path = path - - self._read_led = LED{radius=2, color={0.4, 1, 0.4}} - self._write_led = LED{radius=2, color={1, 0.4, 0.4}} - self._temperature_text = TextLine{align="right"} - self._bar = Bar{} - Rows.init(self, { - Columns{ - Filler{}, - Filler{width=6, widget=Rows{ - Filler{}, - self._read_led, - Filler{height=1}, - self._write_led, - }}, - Filler{width=30, widget=self._temperature_text}, - }, - Filler{height=4}, - self._bar, - Filler{height=25}, - }) - - self._real_height = self.height - self.height = 0 - self._is_mounted = false -end - -function Drive:layout(...) - return self._is_mounted and Rows.layout(self, ...) or {} -end - -function Drive:update() - local was_mounted = self._is_mounted - self._is_mounted = data.is_mounted(self._path) - if self._is_mounted then - if not was_mounted then - self._device, self._physical_device = unpack(data.find_devices()[self._path]) - end - self._bar:set_fill(data.drive_percentage(self._path) / 100) - - local read = data.diskio(self._device, "read", "B") - local read_magnitude = util.log2(read) - self._read_led:set_brightness(read_magnitude / 30) - - local write = data.diskio(self._device, "write", "B") - local write_magnitude = util.log2(write) - self._write_led:set_brightness(write_magnitude / 30) - - local temperature = data.device_temperatures()[self._physical_device] - if temperature then - self._bar.color = {w.temperature_color(temperature, 35, 65)} - self._temperature_text:set_text(math.floor(temperature + 0.5) .. "°C") - else - self._bar.color = {0.8, 0.8, 0.8} - self._temperature_text:set_text("––––") - end - self.height = self._real_height - else - self.height = 0 - end - return self._is_mounted ~= was_mounted -end - -return w diff --git a/src/widgets/core.lua b/src/widgets/core.lua new file mode 100644 index 0000000..93b62ae --- /dev/null +++ b/src/widgets/core.lua @@ -0,0 +1,475 @@ +--- A collection of Widget classes +-- @module widget_core +-- @alias wc + +pcall(function() require('cairo') end) + +local data = require('src/data') +local util = require('src/util') +local ch = require('src/cairo_helpers') + +-- lua 5.1 to 5.3 compatibility +local unpack = unpack or table.unpack -- luacheck: read_globals unpack table + +local sin, cos, tan, PI = math.sin, math.cos, math.tan, math.pi +local floor, ceil, clamp = math.floor, math.ceil, util.clamp + +w = { + --- Font used by widgets if no other is specified. + -- @string default_font_family + default_font_family = "Ubuntu", + + --- Font size used by widgets if no other is specified. + -- @int default_font_size + default_font_size = 10, + + --- Text color used by widgets if no other is specified. + -- @tfield {number,number,number,number} default_text_color + default_text_color = {.94, .94, .94, 1}, -- ~fafafa + + --- Color used to draw some widgets if no other is specified. + -- @tfield {number,number,number,number} default_graph_color + default_graph_color = {.4, 1, 1, 1}, +} + +temperature_colors = { + w.default_graph_color, + {.5, 1, .8}, + {.7, .9, .6}, + {1, .9, .4}, + {1, .6, .2}, + {1, .2, .2}, +} + +--- Generate a temperature based color. +-- Colors are chosen based on float offset in a pre-defined color gradient. +-- @number temperature current temperature (or any other type of numeric value) +-- @number low threshold for lowest temperature / coolest color +-- @number high threshold for highest temperature / hottest color +function w.temperature_color(temperature, low, high) + -- defaults in case temperature is nil + local cool = temperature_colors[1] + local hot = temperature_colors[1] + local weight = 0 + if type(temperature) == "number" and temperature > -math.huge and temperature < math.huge then + local idx = (temperature - low) / (high - low) * (#temperature_colors - 1) + 1 + weight = idx - floor(idx) + cool = temperature_colors[clamp(1, #temperature_colors, floor(idx))] + hot = temperature_colors[clamp(1, #temperature_colors, ceil(idx))] + end + return cool[1] + weight * (hot[1] - cool[1]), + cool[2] + weight * (hot[2] - cool[2]), + cool[3] + weight * (hot[3] - cool[3]) +end + + +--- Root widget wrapper +-- Takes care of managing layout reflows and background caching. +-- @type Renderer +local Renderer = util.class() +w.Renderer = Renderer + +--- +-- @tparam table args table of options +-- @tparam Widget args.root The Widget subclass that should be rendered, +-- usually a Rows widget +-- @int args.width Width of the surface that should be covered +-- @int args.height Height of the surface that should be covered +function Renderer:init(args) + self._root = args.root + self._width = args.width + self._height = args.height + self._background_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + args.width, + args.height) +end + +--- Layout all Widgets and cache their backgrounds. +-- Call this once to create the initial layout. +-- Will be called again automatically each time the layout changes. +function Renderer:layout() + local widgets = self._root:layout(self._width, self._height) or {} + table.insert(widgets, 1, {self._root, 0, 0, self._width, self._height}) + + self._background_widgets = {} + self._update_widgets = {} + self._render_widgets = {} + for widget, x, y, _width, _height in util.imap(unpack, widgets) do + if widget.render_background then + local wsr = cairo_surface_create_for_rectangle(self._background_surface, + floor(x),floor(y),floor(_width),floor(_height)) + table.insert(self._background_widgets, {widget, wsr}) + end + if widget.render then + local wsr = cairo_surface_create_for_rectangle(self._background_surface, + floor(x),floor(y),floor(_width),floor(_height)) + local wcr = cairo_create(wsr) + table.insert(self._render_widgets, {widget, wsr}) + end + if widget.update then + table.insert(self._update_widgets, widget) + end + end + + local cr = cairo_create(self._background_surface) + -- clear surface + cairo_save(cr) + cairo_set_source_rgba(cr, 0, 0, 0, 0) + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE) + cairo_paint(cr) + cairo_restore(cr) + + for widget, wsr in util.imap(unpack, self._background_widgets) do + local wcr = cairo_create(wsr) + cairo_save(wcr) + widget:render_background(wcr) + cairo_restore(wcr) + cairo_destroy(wcr) + end + + if DEBUG then + local version_info = table.concat{"conky ", conky_version, + " ", _VERSION, + " cairo ", cairo_version_string()} + cairo_set_source_rgba(cr, 1, 0, 0, 1) + ch.set_font(cr, "Ubuntu", 8) + ch.write_left(cr, 0, 8, version_info) + for _, x, y, width, height in util.imap(unpack, widgets) do + if width * height ~= 0 then + cairo_rectangle(cr, x, y, width, height) + end + end + cairo_set_line_width(cr, 1) + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) + cairo_set_source_rgba(cr, 1, 0, 0, 0.33) + cairo_stroke(cr) + end + + cairo_destroy(cr) +end + +--- Update all Widgets +-- @int update_count Conky's $updates +function Renderer:update(update_count) + local reflow = false + for _, widget in ipairs(self._update_widgets) do + reflow = widget:update(update_count) or reflow + end + if reflow then + self:layout() + end +end + +function Renderer:paint_background(cr) + cairo_set_source_surface(cr, self._background_surface, 0, 0) + cairo_paint(cr) + for widget, wsr in util.imap(unpack, self._background_widgets) do + local wcr = cairo_create(wsr) + -- clear surface + cairo_save(wcr) + cairo_set_source_rgba(wcr, 0, 0, 0, 0) + cairo_set_operator(wcr, CAIRO_OPERATOR_SOURCE) + cairo_paint(wcr) + cairo_restore(wcr) + cairo_save(wcr) + widget:render_background(wcr) + cairo_restore(wcr) + cairo_destroy(wcr) + end +end + +--- Render to the given context +-- @tparam cairo_t cr +function Renderer:render(cr) + for widget, wsr in util.imap(unpack, self._render_widgets) do + local wcr = cairo_create(wsr) + if not widget.render_background then + -- clear surface + cairo_save(wcr) + cairo_set_source_rgba(wcr, 0, 0, 0, 0) + cairo_set_operator(wcr, CAIRO_OPERATOR_SOURCE) + cairo_paint(wcr) + cairo_restore(wcr) + end + widget:render(wcr) + cairo_destroy(wcr) + end +end + + +--- Base Widget class. +-- @type Widget +local Widget = util.class() +w.Widget = Widget + +--- Set a width if the Widget should have a fixed width. +-- Omit (=nil) if width should be adjusted dynamically. +-- @int Widget.width + +--- Set a height if the Widget should have a fixed height. +-- Omit (=nil) if height should be adjusted dynamically. +-- @int Widget.height + +--- Called at least once to inform the widget of the width and height +-- it may occupy. +-- @tparam int width +-- @tparam int height +function Widget:layout(width, height) end -- luacheck: no unused + +--- Called at least once to allow the widget to draw static content. +-- @function Widget:render_background +-- @tparam cairo_t cr Cairo context for background rendering +-- (to be cached by the `Renderer`) + +--- Called before each call to `Widget:render`. +-- If this function returns a true-ish value, a reflow will be triggered. +-- Since this involves calls to all widgets' :layout functions, +-- reflows should be used sparingly. +-- @function Widget:update +-- @int update_count Conky's $updates +-- @treturn ?bool true(-ish) if a layout reflow should be triggered, causing +-- all `Widget:layout` and `Widget:render_background` methods +-- to be called again + +--- Called once per update to do draw dynamic content. +-- @function Widget:render +-- @tparam cairo_t cr + + +--- Basic collection of widgets. +-- Rows are drawn in a vertical stack starting at the top of the drawble +-- surface. +-- @type Rows +local Rows = util.class(Widget) +w.Rows = Rows + +--- @tparam {Widget,...} widgets +function Rows:init(widgets) + self._widgets = widgets + local width = 0 + self._min_height = 0 + self._fillers = 0 + for _, widget in ipairs(widgets) do + if widget.width then + if widget.width > width then width = widget.width end + end + if widget.height ~= nil then + self._min_height = self._min_height + widget.height + else + self._fillers = self._fillers + 1 + end + end + if self._fillers == 0 then + self.height = self._min_height + end +end + +function Rows:layout(width, height) + self._width = width -- used to draw debug lines + local y = 0 + local children = {} + local filler_height = (height - self._min_height) / self._fillers + for _, widget in ipairs(self._widgets) do + local widget_height = widget.height or filler_height + table.insert(children, {widget, 0, y, width, widget_height}) + local sub_children = widget:layout(width, widget_height) or {} + for _, child in ipairs(sub_children) do + child[3] = child[3] + y + table.insert(children, child) + end + y = y + widget_height + end + return children +end + + +--- Display Widgets side by side +-- @type Columns +local Columns = util.class(Widget) +w.Columns = Columns + +-- reuse an identical function + +--- @tparam {Widget,...} widgets +function Columns:init(widgets) + self._widgets = widgets + self._min_width = 0 + self._fillers = 0 + local height = 0 + local fix_height = false + for _, widget in ipairs(widgets) do + if widget.width ~= nil then + self._min_width = self._min_width + widget.width + else + self._fillers = self._fillers + 1 + end + if widget.height then + fix_height = true + if widget.height > height then height = widget.height end + end + end + if self._fillers == 0 then + self.width = self._min_width + end + if fix_height then + self.height = height + end +end + + +function Columns:layout(width, height) + self._height = height -- used to draw debug lines + local x = 0 + local children = {} + local filler_width = (width - self._min_width) / self._fillers + for _, widget in ipairs(self._widgets) do + local widget_width = widget.width or filler_width + table.insert(children, {widget, x, 0, widget_width, height}) + local sub_children = widget:layout(widget_width, height) or {} + for _, child in ipairs(sub_children) do + child[2] = child[2] + x + table.insert(children, child) + end + x = x + widget_width + end + return children +end + + +--- Leave space between widgets. +-- If either height or width is not specified, the available space +-- inside a Rows or Columns widget will be distributed evenly between Fillers +-- with no fixed height/width. +-- A Filler may contain one other Widget which will have its dimensions +-- restricted to those of the Filler. +-- @type Filler +local Filler = util.class(Widget) +w.Filler = Filler + +--- @tparam ?table args table of options +-- @tparam ?int args.width +-- @tparam ?int args.height +-- @tparam ?Widget args.widget +function Filler:init(args) + if args then + self._widget = args.widget + self.height = args.height or (self._widget and self._widget.height) + self.width = args.width or (self._widget and self._widget.width) + end +end + +function Filler:layout(width, height) + if self._widget then + local children = self._widget:layout(width, height) or {} + table.insert(children, 1, {self._widget, 0, 0, width, height}) + return children + end +end + + +local function side_widths(arg) + arg = arg or 0 + if type(arg) == "number" then + return {top=arg, right=arg, bottom=arg, left=arg} + elseif #arg == 2 then + return {top=arg[1], right=arg[2], bottom=arg[1], left=arg[2]} + elseif #arg == 3 then + return {top=arg[1], right=arg[2], bottom=arg[3], left=arg[2]} + elseif #arg == 4 then + return {top=arg[1], right=arg[2], bottom=arg[3], left=arg[4]} + end +end + + +--- Draw a static border and/or background around/behind another widget. +-- @type Frame +local Frame = util.class(Widget) +w.Frame = Frame + +--- @tparam Widget widget Widget to be wrapped +-- @tparam table args table of options +-- @tparam ?number|{number,...} args.padding Leave some space around the inside +-- of the frame.
    +-- - number: same padding all around.
    +-- - table of two numbers: {top & bottom, left & right}
    +-- - table of three numbers: {top, left & right, bottom}
    +-- - table of four numbers: {top, right, bottom, left} +-- @tparam ?number|{number,...} args.margin Like padding but outside the border. +-- @tparam ?{number,number,number,number} args.background_color +-- @tparam[opt=transparent] ?{number,number,number,number} args.border_color +-- @tparam[opt=0] ?number args.border_width border line width +-- @tparam ?{string,...} args.border_sides any combination of +-- "top", "right", "bottom" and/or "left" +-- (default: all sides) +function Frame:init(widget, args) + self._widget = widget + self._background_color = args.background_color or nil + self._border_color = args.border_color or {0, 0, 0, 0} + self._border_width = args.border_width or 0 + + self._padding = side_widths(args.padding) + self._margin = side_widths(args.margin) + self._border_sides = util.set(args.border_sides or {"top", "right", "bottom", "left"}) + + self._has_background = self._background_color and self._background_color[4] > 0 + self._has_border = self._border_width > 0 + and (not args.border_sides or #args.border_sides > 0) + + self._x_left = self._margin.left + self._padding.left + + (self._border_sides.left and self._border_width or 0) + self._y_top = self._margin.top + self._padding.top + + (self._border_sides.top and self._border_width or 0) + self._x_right = self._margin.right + self._padding.right + + (self._border_sides.right and self._border_width or 0) + self._y_bottom = self._margin.bottom + self._padding.bottom + + (self._border_sides.bottom and self._border_width or 0) + + if widget.width then + self.width = widget.width + self._x_left + self._x_right + end + if widget.height then + self.height = widget.height + self._y_top + self._y_bottom + end +end + +function Frame:layout(width, height) + self._width = width - self._margin.left - self._margin.right + self._height = height - self._margin.top - self._margin.bottom + local inner_width = width - self._x_left - self._x_right + local inner_height = height - self._y_top - self._y_bottom + local children = self._widget:layout(inner_width, inner_height) or {} + for _, child in ipairs(children) do + child[2] = child[2] + self._x_left + child[3] = child[3] + self._y_top + end + table.insert(children, 1, {self._widget, self._x_left, self._y_top, inner_width, inner_height}) + return children +end + +function Frame:render_background(cr) + if self._has_background then + cairo_rectangle(cr, self._margin.left, self._margin.top, self._width, self._height) + cairo_set_source_rgba(cr, unpack(self._background_color)) + cairo_fill(cr) + end + + if self._has_border then + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) + cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE) + cairo_set_source_rgba(cr, unpack(self._border_color)) + cairo_set_line_width(cr, self._border_width) + local x_min = self._margin.left + 0.5 * self._border_width + local y_min = self._margin.top + 0.5 * self._border_width + local x_max = self._margin.left + self._width - 0.5 * self._border_width + local y_max = self._margin.top + self._height - 0.5 * self._border_width + local side, line, move = self._border_sides, cairo_line_to, cairo_move_to + cairo_move_to(cr, x_min, y_min); + (side.top and line or move)(cr, x_max, y_min); + (side.right and line or move)(cr, x_max, y_max); + (side.bottom and line or move)(cr, x_min, y_max); + (side.left and line or move)(cr, x_min, y_min); + cairo_stroke(cr, self._background_color) + end +end + +return w diff --git a/src/widgets/cpu.lua b/src/widgets/cpu.lua new file mode 100644 index 0000000..856ede4 --- /dev/null +++ b/src/widgets/cpu.lua @@ -0,0 +1,383 @@ +--- A collection of CPU Widget classes +-- @module widget_cpu +-- @alias wcpu + +pcall(function() require('cairo') end) + +local data = require('src/data') +local util = require('src/util') +local ch = require('src/cairo_helpers') +local core = require('src/widgets/core') +local Widget = core.Widget + +-- lua 5.1 to 5.3 compatibility +local unpack = unpack or table.unpack -- luacheck: read_globals unpack table + +local sin, cos, tan, PI = math.sin, math.cos, math.tan, math.pi +local floor, ceil, clamp = math.floor, math.ceil, util.clamp + +--- Polygon-style CPU usage & temperature indicator. +-- Looks best for CPUs with 4 to 8 cores but also works for higher numbers. +-- @type Cpu +local Cpu = util.class(Widget) +w.Cpu = Cpu + +--- @tparam table args table of options +-- @int args.cores How many cores does your CPU have? +-- @int args.scale radius of central polygon +-- @int args.gap space between central polygon and outer segments +-- @int args.segment_size radial thickness of outer segments +function Cpu:init(args) + self._cores = args.cores + self._inner_radius = args.inner_radius + self._outer_radius = args.outer_radius + self._gap = args.gap or 4 + + if self._outer_radius then + self.height = 2 * self._outer_radius + self.width = self.height + end +end + +function Cpu:layout(width, height) + if not self._outer_radius then + self._outer_radius = 0.5 * math.min(width, height) + end + if not self._inner_radius then + self._inner_radius = 0.5 * self._outer_radius + end + self._mx = width / 2 + self._my = height / 2 + + self._center_coordinates = {} + self._segment_coordinates = {} + self._gradient_coordinates = {} + local sector_rad = 2 * PI / self._cores + local center_scale = self._inner_radius - 2.5 -- thick stroke + local min = self._inner_radius + local max = self._outer_radius - self._gap + for core = 1, self._cores do + local rad_center = (core - 1) * sector_rad - PI / 2 + local rad_left = rad_center + sector_rad / 2 + local rad_right = rad_center - sector_rad / 2 + local dx_center, dy_center = cos(rad_center), sin(rad_center) + local dx_left, dy_left = cos(rad_left), sin(rad_left) + local dx_right, dy_right = cos(rad_right), sin(rad_right) + self._center_coordinates[2 * core - 1] = self._mx + center_scale * dx_left + self._center_coordinates[2 * core] = self._my + center_scale * dy_left + + -- segment corners + local dx_gap, dy_gap = self._gap * dx_center, self._gap * dy_center + local x1 = self._mx + min * dx_left + dx_gap + local y1 = self._my + min * dy_left + dy_gap + local x2 = self._mx + max * dx_left + dx_gap + local y2 = self._my + max * dy_left + dy_gap + local x3 = self._mx + max * dx_right + dx_gap + local y3 = self._my + max * dy_right + dy_gap + local x4 = self._mx + min * dx_right + dx_gap + local y4 = self._my + min * dy_right + dy_gap + self._segment_coordinates[core] = {x1, y1, x2, y2, x3, y3, x4, y4} + self._gradient_coordinates[core] = {(x1 + x4) / 2, (y1 + y4) / 2, + (x2 + x3) / 2, (y2 + y3) / 2} + end +end + +function Cpu:update() + self._percentages = data.cpu_percentages(self._cores) + self._temperatures = data.cpu_temperatures() +end + +function Cpu:render(cr) + local avg_temperature = util.avg(self._temperatures) + local r, g, b = w.temperature_color(avg_temperature, 30, 80) + + ch.polygon(cr, self._center_coordinates) + cairo_set_line_width(cr, 6) + cairo_set_source_rgba(cr, r, g, b, .33) + cairo_stroke_preserve(cr) + cairo_set_line_width(cr, 1) + cairo_set_source_rgba(cr, 0, 0, 0, .66) + cairo_stroke_preserve(cr) + cairo_set_source_rgba(cr, r, g, b, .18) + cairo_fill(cr) + + cairo_set_source_rgba(cr, r, g, b, .4) + ch.set_font(cr, w.default_font_family, 16, nil, CAIRO_FONT_WEIGHT_BOLD) + ch.write_middle(cr, self._mx + 1, self._my, string.format("%.0f°", avg_temperature)) + + for core = 1, self._cores do + ch.polygon(cr, self._segment_coordinates[core]) + local gradient = cairo_pattern_create_linear(unpack(self._gradient_coordinates[core])) + r, g, b = w.temperature_color(self._temperatures[core], 30, 80) + cairo_set_source_rgba(cr, 0, 0, 0, .4) + cairo_set_line_width(cr, 1.5) + cairo_stroke_preserve(cr) + cairo_set_source_rgba(cr, r, g, b, .4) + cairo_set_line_width(cr, .75) + cairo_stroke_preserve(cr) + + local h_rel = self._percentages[core]/100 + cairo_pattern_add_color_stop_rgba(gradient, 0, r, g, b, .33) + cairo_pattern_add_color_stop_rgba(gradient, h_rel - .045, r, g, b, .75) + cairo_pattern_add_color_stop_rgba(gradient, h_rel, + r * 1.2, g * 1.2, b * 1.2, 1) + if h_rel < .95 then -- prevent pixelated edge + cairo_pattern_add_color_stop_rgba(gradient, h_rel + .045, r, g, b, .33) + cairo_pattern_add_color_stop_rgba(gradient, h_rel + .33, r, g, b, .15) + cairo_pattern_add_color_stop_rgba(gradient, 1, r, g, b, .15) + end + cairo_set_source(cr, gradient) + cairo_pattern_destroy(gradient) + cairo_fill(cr) + end +end + + +--- Round CPU usage & temperature indicator. +-- Best suited for CPUs with high core counts. +-- @type CpuRound +local CpuRound = util.class(Widget) +w.CpuRound = CpuRound + +CpuRound.update = Cpu.update + +--- @tparam table args table of options +-- @int args.cores How many cores does your CPU have? +-- @int args.inner_radius Size of inner circle +-- @int args.outer_radius Max radius for core at 100% +-- @int[opt] args.grid Number of grid lines to draw in the background. +function CpuRound:init(args) + self._cores = args.cores + self._inner_radius = args.inner_radius + self._outer_radius = args.outer_radius + self._grid = args.grid + + if self._outer_radius then + self.height = 2 * self._outer_radius + self.width = self.height + end +end + +function CpuRound:layout(width, height) + if not self._outer_radius then + self._outer_radius = 0.5 * math.min(width, height) + end + if not self._inner_radius then + self._inner_radius = 0.75 * self._outer_radius + end + self._center = {width / 2, height / 2} + local sector_rad = 2 * PI / self._cores + + -- choose control points that best approximate a circle, see + -- https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves + local ctrl_length = 1.3333 * tan(0.25 * sector_rad) + self._points = {} + for core = 1, self._cores do + local rad = (core - 1) * sector_rad + local dx, dy = cos(rad), sin(rad) + self._points[core] = { + dx = dx, + dy = dy, + ctrl_left_dx = dx - dy * ctrl_length, + ctrl_left_dy = dy + dx * ctrl_length, + ctrl_right_dx = dx + dy * ctrl_length, + ctrl_right_dy = dy - dx * ctrl_length, + } + end + self._points[self._cores + 1] = self._points[1] -- easy cycling +end + +function CpuRound:render_background(cr) + if not self._grid or self._grid < 1 then + return + end + local mx, my = unpack(self._center) + local gap = (self._outer_radius - self._inner_radius) / self._grid + for line = 0, self._grid do + local scale = self._inner_radius + gap * line + cairo_move_to(cr, mx + self._points[1].dx * scale, + my + self._points[1].dy * scale) + cairo_arc(cr, mx, my, scale, 0, 2 * PI) + end + for _, point in ipairs(self._points) do + cairo_move_to(cr, mx + point.dx * self._inner_radius, + my + point.dy * self._inner_radius) + cairo_line_to(cr, mx + point.dx * self._outer_radius, + my + point.dy * self._outer_radius) + end + local r, g, b = unpack(w.default_graph_color) + cairo_set_source_rgba(cr, r, g, b, 0.2) + cairo_set_line_width(cr, 1) + cairo_stroke(cr) +end + +function CpuRound:render(cr) + local avg_temperature = util.avg(self._temperatures) + local avg_percentage = util.avg(self._percentages) + local r, g, b = w.temperature_color(avg_temperature, 30, 80) + local mx, my = unpack(self._center) + + -- glow + ch.alpha_gradient_radial(cr, mx, my, self._inner_radius, + mx, my, + self._outer_radius * (1 + 0.5 * avg_percentage / 100), + r, g, b, {0, 0, 0.05, 0.2, 1, 0}) + cairo_paint(cr) + + -- temperature text + cairo_set_source_rgba(cr, r, g, b, 0.5) + ch.set_font(cr, w.default_font_family, 16, nil, CAIRO_FONT_WEIGHT_BOLD) + ch.write_middle(cr, mx + 1, my, string.format("%.0f°", avg_temperature)) + + -- inner fill + cairo_new_path(cr) + cairo_arc(cr, mx, my, self._inner_radius * 0.99, 0, 2 * PI) + ch.alpha_gradient_radial(cr, mx - self._inner_radius * 0.5, + my - self._inner_radius * 0.5, + 0, + mx, my, self._inner_radius, + r, g, b, {0, 0.4, 0.66, 0.15, 1, 0.1}) + cairo_fill(cr) + + -- usage curve + local dr = self._outer_radius - self._inner_radius + for core = 1, self._cores do + local point = self._points[core] + local scale = self._inner_radius + dr * self._percentages[core] / 100 + point.x = mx + point.dx * scale + point.y = my + point.dy * scale + point.ctrl_left_x = mx + point.ctrl_left_dx * scale + point.ctrl_left_y = my + point.ctrl_left_dy * scale + point.ctrl_right_x = mx + point.ctrl_right_dx * scale + point.ctrl_right_y = my + point.ctrl_right_dy * scale + end + cairo_move_to(cr, self._points[1].x, self._points[1].y) + for core = 1, self._cores do + local current, next = self._points[core], self._points[core + 1] + cairo_curve_to(cr, current.ctrl_left_x, + current.ctrl_left_y, + next.ctrl_right_x, + next.ctrl_right_y, + next.x, + next.y) + end + cairo_close_path(cr) + + ch.alpha_gradient_radial(cr, mx, my, self._inner_radius, + mx, my, self._outer_radius, + r, g, b, {0, 0, 0.05, 0.4, 1, 0.9}) + cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT) + cairo_fill_preserve(cr) + cairo_set_source_rgba(cr, r, g, b, 1) + cairo_set_line_width(cr, 0.75) + cairo_stroke(cr) +end + + +--- Visualize cpu-frequencies in a style reminiscent of stacked progress bars. +-- @type CpuFrequencies +local CpuFrequencies = util.class(Widget) +w.CpuFrequencies = CpuFrequencies + +--- @tparam table args table of options +-- @int args.cores How many cores does your CPU have? +-- @number args.min_freq What is your CPU's minimum frequency? +-- @number args.min_freq What is your CPU's maximum frequency? +-- @int[opt=16] args.height Maximum pixel height of the drawn shape +function CpuFrequencies:init(args) + self.cores = args.cores + self.min_freq = args.min_freq + self.max_freq = args.max_freq + self._height = args.height or 16 + self.height = self._height + 13 +end + +function CpuFrequencies:layout(width) + self._width = width - 25 + self._polygon_coordinates = { + 0, self._height * (1 - self.min_freq / self.max_freq), + self._width, 0, + self._width, self._height, + 0, self._height, + } + self._ticks = {} + self._tick_labels = {} + + local df = self.max_freq - self.min_freq + for freq = 1, self.max_freq, .25 do + local x = self._width * (freq - self.min_freq) / df + local big = math.floor(freq) == freq + if big then + table.insert(self._tick_labels, {x, self._height + 11.5, ("%.0f"):format(freq)}) + end + table.insert(self._ticks, {math.floor(x) + .5, + self._height + 2, + big and 3 or 2}) + end +end + +function CpuFrequencies:render_background(cr) + cairo_set_source_rgba(cr, unpack(w.default_text_color)) + ch.set_font(cr, w.default_font_family, w.default_font_size) + ch.write_left(cr, self._width + 5, 0.5 * self._height + 3, "GHz") + + -- shadow outline + ch.polygon(cr, { + self._polygon_coordinates[1] - 1, self._polygon_coordinates[2] - 1, + self._polygon_coordinates[3] + 1, self._polygon_coordinates[4] - 1, + self._polygon_coordinates[5] + 1, self._polygon_coordinates[6] + 1, + self._polygon_coordinates[7] - 1, self._polygon_coordinates[8] + 1, + }) + cairo_set_source_rgba(cr, 0, 0, 0, .4) + cairo_set_line_width(cr, 1) + cairo_stroke(cr) +end + +function CpuFrequencies:update() + self.frequencies = data.cpu_frequencies(self.cores) + self.temperatures = data.cpu_temperatures() +end + +function CpuFrequencies:render(cr) + local r, g, b = w.temperature_color(util.avg(self.temperatures), 30, 80) + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) + cairo_set_line_width(cr, 1) + + -- ticks -- + cairo_set_source_rgba(cr, r, g, b, .66) + for _, tick in ipairs(self._ticks) do + cairo_move_to(cr, tick[1], tick[2]) + cairo_rel_line_to(cr, 0, tick[3]) + end + cairo_stroke(cr) + ch.set_font(cr, w.default_font_family, w.default_font_size) + for _, label in ipairs(self._tick_labels) do + ch.write_centered(cr, label[1], label[2], label[3]) + end + + -- background -- + ch.polygon(cr, self._polygon_coordinates) + cairo_set_source_rgba(cr, r, g, b, .15) + cairo_fill_preserve(cr) + cairo_set_source_rgba(cr, r, g, b, .3) + cairo_stroke_preserve(cr) + + -- frequencies -- + local df = self.max_freq - self.min_freq + for _, frequency in ipairs(self.frequencies) do + local stop = (frequency - self.min_freq) / df + ch.alpha_gradient(cr, 0, 0, self._width, 0, r, g, b, { + 0, 0.01, + stop - .4, 0.015, + stop - .2, 0.05, + stop - .1, 0.1, + stop - .02, 0.2, + stop, 0.6, + stop, 0, + }) + cairo_fill_preserve(cr) + end + cairo_new_path(cr) +end + +return w diff --git a/src/widgets/drive.lua b/src/widgets/drive.lua new file mode 100644 index 0000000..48bd302 --- /dev/null +++ b/src/widgets/drive.lua @@ -0,0 +1,93 @@ +--- A collection of Disk Drive Widget classes +-- @module widget_drive +-- @alias wdrive + +pcall(function() require('cairo') end) + +local data = require('src/data') +local util = require('src/util') +local ch = require('src/cairo_helpers') +local core = require('src/widgets/core') +local graph = require('src/widgets/graph') +local text = require('src/widgets/text') +local Widget = core.Widget + +-- lua 5.1 to 5.3 compatibility +local unpack = unpack or table.unpack -- luacheck: read_globals unpack table + +local sin, cos, tan, PI = math.sin, math.cos, math.tan, math.pi +local floor, ceil, clamp = math.floor, math.ceil, util.clamp + +--- Visualize drive usage and temperature in a colorized Bar. +-- Also writes temperature as text. +-- This widget is exptected to be combined with some special conky.text. +-- @type Drive +local Drive = util.class(Rows) +w.Drive = Drive + +--- @string path e.g. "/home" +function Drive:init(path) + self._path = path + + self._read_led = graph.LED{radius=2, color={0.4, 1, 0.4}} + self._write_led = graph.LED{radius=2, color={1, 0.4, 0.4}} + self._temperature_text = text.TextLine{align="right"} + self._bar = core.Bar{} + core.Rows.init(self, { + core.Columns{ + core.Filler{}, + core.Filler{width=6, widget=core.Rows{ + core.Filler{}, + self._read_led, + core.Filler{height=1}, + self._write_led, + }}, + core.Filler{width=30, widget=self._temperature_text}, + }, + core.Filler{height=4}, + self._bar, + core.Filler{height=25}, + }) + + self._real_height = self.height + self.height = 0 + self._is_mounted = false +end + +function Drive:layout(...) + return self._is_mounted and Rows.layout(self, ...) or {} +end + +function Drive:update() + local was_mounted = self._is_mounted + self._is_mounted = data.is_mounted(self._path) + if self._is_mounted then + if not was_mounted then + self._device, self._physical_device = unpack(data.find_devices()[self._path]) + end + self._bar:set_fill(data.drive_percentage(self._path) / 100) + + local read = data.diskio(self._device, "read", "B") + local read_magnitude = util.log2(read) + self._read_led:set_brightness(read_magnitude / 30) + + local write = data.diskio(self._device, "write", "B") + local write_magnitude = util.log2(write) + self._write_led:set_brightness(write_magnitude / 30) + + local temperature = data.device_temperatures()[self._physical_device] + if temperature then + self._bar.color = {w.temperature_color(temperature, 35, 65)} + self._temperature_text:set_text(math.floor(temperature + 0.5) .. "°C") + else + self._bar.color = {0.8, 0.8, 0.8} + self._temperature_text:set_text("––––") + end + self.height = self._real_height + else + self.height = 0 + end + return self._is_mounted ~= was_mounted +end + +return w diff --git a/src/widgets/gpu.lua b/src/widgets/gpu.lua new file mode 100644 index 0000000..ae2d964 --- /dev/null +++ b/src/widgets/gpu.lua @@ -0,0 +1,91 @@ +--- A collection of GPU Widget classes +-- @module widget_gpu +-- @alias wgpu + +pcall(function() require('cairo') end) + +local data = require('src/data') +local util = require('src/util') +local ch = require('src/cairo_helpers') +local core = require('src/widgets/core') +local mem = require('src/widgets/memory') +local Widget = core.Widget + +-- lua 5.1 to 5.3 compatibility +local unpack = unpack or table.unpack -- luacheck: read_globals unpack table + +local sin, cos, tan, PI = math.sin, math.cos, math.tan, math.pi +local floor, ceil, clamp = math.floor, math.ceil, util.clamp + +--- Compound widget to display GPU and VRAM usage. +-- @type Gpu +local Gpu = util.class(core.Rows) +w.Gpu = Gpu + +--- no options +function Gpu:init() + self._usebar = core.Bar{ticks={.25, .5, .75}, unit="%"} + + local _, mem_total = data.gpu_memory() + self._membar = mem.MemoryBar{total=mem_total / 1024} + self._membar.update = function() + self._membar:set_used(data.gpu_memory() / 1024) + end + core.Rows.init(self, {self._usebar, core.Filler{height=4}, self._membar}) +end + +function Gpu:update() + self._usebar:set_fill(data.gpu_percentage() / 100) + + local color = {w.temperature_color(data.gpu_temperature(), 30, 80)} + self._usebar.color = color + self._membar.color = color +end + +--- Table of processes for the GPU, sorted by VRAM usage +-- @type GpuTop +local GpuTop = util.class(Widget) +w.GpuTop = GpuTop + +--- @tparam table args table of options +-- @tparam[opt=5] ?int args.lines how many processes to display +-- @tparam ?string args.font_family +-- @tparam ?number args.font_size +-- @tparam ?{number,number,number} args.color (default: `default_text_color`) +function GpuTop:init(args) + self._lines = args.lines or 5 + self._font_family = args.font_family or w.default_font_family + self._font_size = args.font_size or w.default_font_size + self._color = args.color or w.default_text_color + + local extents = ch.font_extents(self._font_family, self._font_size) + self._line_height = extents.height + self.height = self._lines * self._line_height + local line_spacing = extents.height - (extents.ascent + extents.descent) + -- try to match conky's line spacing: + self._baseline_offset = extents.ascent + 0.5 * line_spacing + 1 +end + +function GpuTop:layout(width) + self._width = width +end + +function GpuTop:update() + self._processes = data.gpu_top() +end + +function GpuTop:render(cr) + ch.set_font(cr, self._font_family, self._font_size) + cairo_set_source_rgba(cr, unpack(self._color)) + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) + + local lines = math.min(self._lines, #self._processes) + local y = self._baseline_offset + for i = 1, lines do + ch.write_left(cr, 0, y, self._processes[i][1]) + ch.write_right(cr, self._width, y, self._processes[i][2] .. " MiB") + y = y + self._line_height + end +end + +return w diff --git a/src/widgets/graph.lua b/src/widgets/graph.lua new file mode 100644 index 0000000..fea029b --- /dev/null +++ b/src/widgets/graph.lua @@ -0,0 +1,325 @@ +--- A collection of Basic Graph and indicator Widget classes +-- @module widget_graph +-- @alias wg + +pcall(function() require('cairo') end) + +local data = require('src/data') +local util = require('src/util') +local ch = require('src/cairo_helpers') +local core = require('src/widgets/core') +local Widget = core.Widget + +-- lua 5.1 to 5.3 compatibility +local unpack = unpack or table.unpack -- luacheck: read_globals unpack table + +local sin, cos, tan, PI = math.sin, math.cos, math.tan, math.pi +local floor, ceil, clamp = math.floor, math.ceil, util.clamp + +--- Progress-bar like box, similar to conky's bar. +-- Can have small and big ticks for visual clarity, +-- and a unit (static, up to 3 characters) written behind the end. +-- @type Bar +local Bar = util.class(Widget) +w.Bar = Bar + +--- @tparam table args table of options +-- @tparam[opt=6] int args.thickness vertical size of the bar +-- @tparam ?string args.unit to be drawn behind the bar - 3 characters will fit +-- @tparam ?{number,...} args.ticks relative offsets (between 0 and 1) of ticks +-- @tparam ?int args.big_ticks multiple of ticks to be drawn longer +-- @tparam ?{number,number,number} args.color (default: `default_graph_color`) +function Bar:init(args) + self._ticks = args.ticks + self._big_ticks = args.big_ticks + self._unit = args.unit + self._thickness = (args.thickness or 4) + self.height = self._thickness + 2 + self.color = args.color or w.default_graph_color + + if self._ticks then + self.height = self.height + (self._big_ticks and 3 or 2) + end + if self._unit then + self.height = math.max(self.height, 8) -- line_height + end + + self._fraction = 0 +end + +function Bar:layout(width) + self._width = width - (self._unit and 20 or 0) - 2 + self._tick_coordinates = {} + if self._ticks then + local x, tick_length + for i, frac in ipairs(self._ticks) do + x = math.floor(frac * self._width) + tick_length = 3 + if self._big_ticks then + if i % self._big_ticks == 0 then + tick_length = 3 + else + tick_length = 2 + end + end + table.insert(self._tick_coordinates, {x, self._thickness + 1, tick_length}) + end + end +end + +function Bar:render_background(cr) + if self._unit then + cairo_set_source_rgba(cr, unpack(w.default_text_color)) + ch.set_font(cr, w.default_font_family, w.default_font_size) + ch.write_left(cr, self._width + 5, 6, self._unit) + end + -- fake shadow border + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) + cairo_set_line_width(cr, 1) + cairo_rectangle(cr, 0, 0, self._width + 1, self._thickness + 1) + cairo_set_source_rgba(cr, 0, 0, 0, .66) + cairo_stroke(cr) +end + +--- Set the fill-ratio of the bar +-- @number fraction between 0 and 1 +function Bar:set_fill(fraction) + self._fraction = fraction +end + +function Bar:render(cr) + local r, g, b = unpack(self.color) + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) + cairo_set_line_width(cr, 1) + + cairo_rectangle(cr, 0, 0, self._width, self._thickness) + ch.alpha_gradient(cr, 0, 0, self._width, 0, r, g, b, { + self._fraction - 0.33, 0.33, + self._fraction - 0.08, 0.66, + self._fraction - 0.01, 0.75, + self._fraction, 1, + self._fraction + 0.01, 0.2, + self._fraction + 0.1, 0.1, + 1, 0.15, + }) + cairo_fill(cr) + + -- border + cairo_rectangle(cr, 1, 1, self._width - 1, self._thickness - 1) + cairo_set_source_rgba(cr, r, g, b, .2) + cairo_stroke(cr) + + -- ticks + for _, tick in ipairs(self._tick_coordinates) do + cairo_move_to(cr, tick[1], tick[2]) + cairo_rel_line_to(cr, 0, tick[3]) + end + cairo_set_source_rgba(cr, r, g, b, .5) + cairo_stroke(cr) +end + +--- Track changing data; similar to conky's graphs. +-- @type Graph +local Graph = util.class(Widget) +w.Graph = Graph + +--- @tparam table args table of options +-- @tparam number args.max maximum expected value to be represented; +-- may be expanded automatically as need arises +-- @int[opt=60] args.data_points how many values to store +-- @bool[opt=false] args.upside_down Draw graph from top to bottom? +-- @number[opt=0.5] args.smoothness Bézier curves smoothness. +-- Set to 0 to draw straight lines instead, +-- which may be slightly faster. +-- @int[opt] args.width fix width in pixels +-- @int[opt] args.height fixeheight in pixels +-- @tparam ?{number,number,number} args.color (default: `default_graph_color`) +function Graph:init(args) + self._max = args.max + self._data = util.CycleQueue(args.data_points or 60) + self._upside_down = args.upside_down or false + self._smoothness = args.smoothness or 0.5 + self.color = args.color or w.default_graph_color + self.width = args.width + self.height = args.height +end + +function Graph:layout(width, height) + self._width = width - 2 + self._height = height - 2 + self._x_scale = (width - 2) / (self._data.length - 1) + self._y_scale = (height - 3) / self._max + if self._upside_down then + self._y_scale = -self._y_scale + self._y = -0.5 + else + self._y = self._height - 0.5 + end +end + +function Graph:render_background(cr) + local r, g, b = unpack(self.color) + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) + + -- background + cairo_rectangle(cr, 0, 0, self._width + 1, self._height + 1) + ch.alpha_gradient(cr, 0, 0, 0, self._height, r, g, b, {0, .15, 1, .03}) + cairo_fill(cr) + + -- grid + cairo_set_line_width(cr, 1) + cairo_set_source_rgba(cr, r, g, b, .0667) + cairo_rectangle(cr, 1, 1, self._width - 1, self._height - 1) + local gridsize = 5 + for row = gridsize + 1, self._height, gridsize do + cairo_move_to(cr, 1, row) + cairo_line_to(cr, self._width, row) + end + for column = gridsize + 1, self._width, gridsize do + cairo_move_to(cr, column, 1) + cairo_line_to(cr, column, self._height) + end + cairo_stroke(cr) + for row = gridsize + 1, self._height, gridsize do + for column = gridsize + 1, self._width, gridsize do + cairo_rectangle(cr, column - 0.5, row - 0.5, 0.5, 0.5) + end + end + cairo_set_source_rgba(cr, r, g, b, .15) + cairo_stroke(cr) +end + +--- Append the latest value to be shown - this will displace the oldest value +-- @number value if value > args.max then the graphs vertical scale will be +-- adjusted, causing it to get squished +function Graph:add_value(value) + self._data:put(value) + if value > self._max then + self._max = value + self:layout(self._width + 2, self._height + 2) + end +end + +function Graph:_line_path(cr) + local current_max = 0 + cairo_move_to(cr, 0.5, self._y - self._data[1] * self._y_scale) + for idx, val in self._data:__ipairs() do + if current_max < val then current_max = val end + if idx > 1 then + cairo_line_to(cr, 0.5 + (idx - 1) * self._x_scale, + self._y - val * self._y_scale) + end + end + return current_max +end + +function Graph:_berzier_path(cr) + local current_max = 0 + local prev_x, prev_y = 0.5, self._y - self._data[1] * self._y_scale + cairo_move_to(cr, prev_x, prev_y) + for idx, val in self._data:__ipairs() do + if current_max < val then current_max = val end + if idx > 1 then + local current_x = 0.5 + (idx - 1) * self._x_scale + local current_y = self._y - val * self._y_scale + local x1 = prev_x + self._smoothness * self._x_scale + local x2 = current_x - self._smoothness * self._x_scale + cairo_curve_to(cr, x1, prev_y, x2, current_y, current_x, current_y) + prev_x, prev_y = current_x, current_y + end + end + return current_max +end + +function Graph:render(cr) + local r, g, b = unpack(self.color) + cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT) + cairo_set_source_rgba(cr, r, g, b, 1) + cairo_set_line_width(cr, 0.5) + + local current_max = self._smoothness > 0 and self:_berzier_path(cr) + or self:_line_path(cr) + + if current_max > 0 then -- fill under graph + cairo_stroke_preserve(cr) + cairo_line_to(cr, self._width, self._y) + cairo_line_to(cr, 0, self._y) + cairo_close_path(cr) + ch.alpha_gradient(cr, 0, self._y - current_max * self._y_scale, 0, self._y, + r, g, b, {0, .66, .5, .33, 1, .25}) + cairo_fill(cr) + else + cairo_stroke(cr) + end +end + +--- Round light indicator for minimalistic feedback. +-- @type LED +local LED = util.class(Widget) +w.LED = LED + +--- @tparam table args table of options +-- @number args.radius size of the LED +-- @number[opt=0] args.brightness between 0 and 1, how "on" should the LED be? +-- Can be changed later with `LED:set_brightness` +-- @tparam ?{number,number,number} args.color color of the LED, +-- can be changed later with `LED:set_color`. +-- (default: `default_graph_color`) +-- @tparam ?{number,number,number,number} args.background_color mostly visible +-- when the LED is off. This allows you to choose +-- a neutral background if you plan on changing +-- the light color via `LED:set_color`. +-- (default: darkened `args.color`) +function LED:init(args) + assert(args.radius) + self._radius = args.radius + self.width = self._radius * 2 + self.height = self._radius * 2 + self._brightness = args.brightness or 0 + self._color = args.color or w.default_graph_color + if args.background_color then + self._background_color = args.background_color + else + local r, g, b = unpack(self._color) + self._background_color = {0.2 * r, 0.2 * g, 0.2 * b, 0.75} + end +end + +--- @number brightness between 0 and 1 +function LED:set_brightness(brightness) + self._brightness = clamp(0, 1, brightness) +end + +--- @tparam ?{number,number,number} color +function LED:set_color(color) + self._color = color +end + +function LED:layout(width, height) + self._mx = width / 2 + self._my = height / 2 +end + +function LED:render_background(cr) + cairo_arc(cr, self._mx, self._my, self._radius, 0, 360) + cairo_set_source_rgba(cr, unpack(self._background_color)) + cairo_fill(cr) +end + +function LED:render(cr) + if self._brightness > 0 then + local r, g, b = unpack(self._color) + local gradient = cairo_pattern_create_radial(self._mx, self._my, 0, + self._mx, self._my, self._radius) + cairo_pattern_add_color_stop_rgba(gradient, 0, r, g, b, 1 * self._brightness) + cairo_pattern_add_color_stop_rgba(gradient, 0.5, r, g, b, 0.5 * self._brightness) + cairo_pattern_add_color_stop_rgba(gradient, 1, r, g, b, 0.1 * self._brightness) + cairo_set_source(cr, gradient) + cairo_pattern_destroy(gradient) + + cairo_arc(cr, self._mx, self._my, self._radius, 0, 360) + cairo_fill(cr) + end +end + +return w diff --git a/src/widgets/memory.lua b/src/widgets/memory.lua new file mode 100644 index 0000000..3fc890f --- /dev/null +++ b/src/widgets/memory.lua @@ -0,0 +1,138 @@ +--- A collection of Widget classes +-- @module widget_memory +-- @alias wmem + +pcall(function() require('cairo') end) + +local data = require('src/data') +local util = require('src/util') +local ch = require('src/cairo_helpers') +local core = require('src/widgets/core') +local graph = require('src/widgets/graph') +local Widget = core.Widget + +-- lua 5.1 to 5.3 compatibility +local unpack = unpack or table.unpack -- luacheck: read_globals unpack table + +local sin, cos, tan, PI = math.sin, math.cos, math.tan, math.pi +local floor, ceil, clamp = math.floor, math.ceil, util.clamp + + +--- Specialized unit-based Bar. +-- @type MemoryBar +local MemoryBar = util.class(graph.Bar) +w.MemoryBar = MemoryBar + +--- @tparam table args table of options +-- @tparam ?number args.total Total amount of memory to be represented +-- by this bar. If greater than 8, ticks will be +-- drawn. If omitted, total RAM will be used, +-- however no ticks can be drawn. +-- @tparam[opt="GiB"] string args.unit passed to `Bar:init` +-- @tparam ?int args.thickness passed to `Bar:init` +-- @tparam ?{number,number,number} args.color passed to `Bar:init` +function MemoryBar:init(args) + self._total = args.total + local ticks, big_ticks + if self._total then + local max_tick = floor(self._total) + ticks = util.range(1 / self._total, max_tick / self._total, 1 / self._total) + big_ticks = max_tick > 8 and 4 or nil + end + graph.Bar.init(self, {ticks=ticks, + big_ticks=big_ticks, + unit=args.unit or "GiB", + thickness=args.thickness, + color=args.color}) +end + +--- Set the amount of used memory as an absolute value. +-- @number used should be between 0 and args.total +function MemoryBar:set_used(used) + self:set_fill(used / self._total) +end + +function MemoryBar:update() + local used, _, _, total = data.memory("GiB") + self:set_fill(used / total) +end + +--- Visualize memory usage in a randomized grid. +-- Does not represent actual distribution of used memory. +-- Also shows buffere/cache memory at reduced brightness. +-- @type MemoryGrid +local MemoryGrid = util.class(Widget) +w.MemoryGrid = MemoryGrid + +--- @tparam table args table of options +-- @tparam ?int args.rows Number of rows to draw. +-- For nil it will be determined based on Widget height. +-- @tparam ?int args.columns Number of columns to draw. +-- For nil it will be determined based on Widget width. +-- @tparam[opt=2] ?int args.point_size edge length of individual squares +-- @tparam[opt=1] ?int args.gap space between squares +-- @tparam[opt=true] ?bool args.shuffle randomize? +-- @tparam ?{number,number,number} args.color (default: `default_graph_color`) +function MemoryGrid:init(args) + self._rows = args.rows + self._columns = args.columns + self._point_size = args.point_size or 2 + self._gap = args.gap or 1 + self._shuffle = args.shuffle == nil and true or args.shuffle + self._color = args.color or w.default_graph_color + if self._rows then + self.height = self._rows * self._point_size + (self._rows - 1) * self._gap + end + if self._columns then + self.width = self._columns * self._point_size + (self._columns - 1) * self._gap + end +end + +function MemoryGrid:layout(width, height) + local point_plus_gap = self._point_size + self._gap + local columns = self._columns or math.floor(width / point_plus_gap) + local rows = self._rows or math.floor(height / point_plus_gap) + local left = 0.5 * (width - columns * point_plus_gap + self._gap) + self._coordinates = {} + for col = 0, columns - 1 do + for row = 0, rows - 1 do + table.insert(self._coordinates, {col * point_plus_gap + left, + row * point_plus_gap, + self._point_size, self._point_size}) + end + end + if self._shuffle == nil or self._shuffle then + util.shuffle(self._coordinates) + end +end + +function MemoryGrid:update() + self._used, self._easyfree, self._free, self._total = data.memory("GiB") +end + +function MemoryGrid:render(cr) + if self._total <= 0 then return end -- TODO figure out why this happens + local total_points = #self._coordinates + local used_points = math.floor(total_points * self._used / self._total + 0.5) + local free_points = math.floor(total_points * self._free / self._total + 0.5) + local r, g, b = unpack(self._color) + + cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE) + for i = 1, used_points do + cairo_rectangle(cr, unpack(self._coordinates[i])) + end + cairo_set_source_rgba(cr, r, g, b, .8) + cairo_fill(cr) + for i = used_points, total_points - free_points do + cairo_rectangle(cr, unpack(self._coordinates[i])) + end + cairo_set_source_rgba(cr, r, g, b, .35) + cairo_fill(cr) + for i = total_points - free_points, total_points do + cairo_rectangle(cr, unpack(self._coordinates[i])) + end + cairo_set_source_rgba(cr, r, g, b, .1) + cairo_fill(cr) +end + +return w diff --git a/src/widgets/network.lua b/src/widgets/network.lua new file mode 100644 index 0000000..c86fe44 --- /dev/null +++ b/src/widgets/network.lua @@ -0,0 +1,44 @@ +--- A collection of Network Widget classes +-- @module widget_network +-- @alias wnet + +pcall(function() require('cairo') end) + +local data = require('src/data') +local util = require('src/util') +local ch = require('src/cairo_helpers') +local core = require('src/widgets/core') +local graph = require('src/widgets/graph') +local Widget = core.Widget + +-- lua 5.1 to 5.3 compatibility +local unpack = unpack or table.unpack -- luacheck: read_globals unpack table + +local sin, cos, tan, PI = math.sin, math.cos, math.tan, math.pi +local floor, ceil, clamp = math.floor, math.ceil, util.clamp + +--- Graphs for up- and download speed. +-- This widget assumes that your conky.text adds some text between the graphs. +-- @type Network +local Network = util.class(core.Rows) +w.Network = Network + +--- @tparam table args table of options +-- @string args.interface e.g. "eth0" +-- @tparam ?int args.graph_height passed to `Graph:init` +-- @number[opt=1024] args.downspeed passed as args.max to download speed graph +-- @number[opt=1024] args.upspeed passed as args.max to upload speed graph +function Network:init(args) + self.interface = args.interface + self._downspeed_graph = graph.Graph{height=args.graph_height, max=args.downspeed or 1024} + self._upspeed_graph = graph.Graph{height=args.graph_height, max=args.upspeed or 1024} + core.Rows.init(self, {self._downspeed_graph, core.Filler{height=31}, self._upspeed_graph}) +end + +function Network:update() + local down, up = data.network_speed(self.interface) + self._downspeed_graph:add_value(down) + self._upspeed_graph:add_value(up) +end + +return w diff --git a/src/widgets/text.lua b/src/widgets/text.lua new file mode 100644 index 0000000..378c6b3 --- /dev/null +++ b/src/widgets/text.lua @@ -0,0 +1,152 @@ +--- A collection of Widget classes +-- @module widget_text +-- @alias wt + +pcall(function() require('cairo') end) + +local data = require('src/data') +local util = require('src/util') +local ch = require('src/cairo_helpers') +local core = require('src/widgets/core') +local Widget = core.Widget + +-- lua 5.1 to 5.3 compatibility +local unpack = unpack or table.unpack -- luacheck: read_globals unpack table +local floor, ceil, clamp = math.floor, math.ceil, util.clamp + + +--- Common (abstract) base class for `StaticText` and `TextLine`. +-- @type Text +local Text = util.class(Widget) +w.Text = Text + +local write_aligned = {left = ch.write_left, + center = ch.write_centered, + right = ch.write_right} + +--- @tparam table args table of options +-- @tparam ?string args.align "left" (default), "center" or "right" +-- @tparam[opt=w.default_font_family] ?string args.font_family +-- @tparam[opt=w.default_font_size] ?number args.font_size +-- @tparam[opt=CAIRO_FONT_SLANT_NORMAL] ?cairo_font_slant_t args.font_slant +-- @tparam[opt=CAIRO_FONT_WEIGHT_NORMAL] ?cairo_font_weight_t args.font_weight +-- @tparam ?{number,number,number,number} args.color (default: `default_text_color`) +function Text:init(args) + assert(getmetatable(self) ~= Text, "Cannot instanciate class Text directly.") + self._align = args.align or "left" + self._font_family = args.font_family or w.default_font_family + self._font_size = args.font_size or w.default_font_size + self._font_slant = args.font_slant or CAIRO_FONT_SLANT_NORMAL + self._font_weight = args.font_weight or CAIRO_FONT_WEIGHT_NORMAL + self._color = args.color or w.default_text_color + + self._write_fn = write_aligned[self._align] + + -- try to match conky's line spacing: + local font_extents = ch.font_extents(self._font_family, self._font_size, + self._font_slant, self._font_weight) + self._line_height = font_extents.height + 1 + + local line_spacing = font_extents.height - (font_extents.ascent + font_extents.descent) + self._baseline_offset = font_extents.ascent + 0.5 * line_spacing + 1 +end + +function Text:layout(width) + if self._align == "center" then + self._x = 0.5 * width + elseif self._align == "left" then + self._x = 0 + else -- self._align == "right" + self._x = width + end +end + +--- Draw text substuting in Conky variables. +-- Text line will be updated on each cycle as per Conky's text +-- Section, some variables such as formatting and positioning +-- may not be honored. +-- @type ConkyParse +local ConkyParse = util.class(Text) +w.ConkyParse = ConkyParse + +--- @string text Text to be displayed, can include conky variables. +--- @tparam table args table of options, see `Text:init` +function ConkyParse:init(text, args) + Text.init(self, args) + + self._lines = {} + local _, line_count = text:gsub("[^\n]*", function(line) + table.insert(self._lines, line) + end) + self.height = line_count * self._line_height +end + +function ConkyParse:render(cr) + ch.set_font(cr, self._font_family, self._font_size, self._font_slant, + self._font_weight) + cairo_set_source_rgba(cr, unpack(self._color)) + for i, line in ipairs(self._lines) do + local y = self._baseline_offset + (i - 1) * self._line_height + self._write_fn(cr, self._x, y, conky_parse(line)) + end +end + +--- Draw some unchangeable text. +-- Use this widget for text that will never be updated.Text +-- @type StaticText +local StaticText = util.class(Text) +w.StaticText = StaticText + +--- @string text Text to be displayed. +-- @tparam ?table args table of options, see `Text:init` +function StaticText:init(text, args) + Text.init(self, args or {}) + + self._lines = {} + text = text .. "\n" + + for line in text:gmatch("(.-)\n") do + table.insert(self._lines, line) + end + + self.height = #self._lines * self._line_height +end + +function StaticText:render_background(cr) + ch.set_font(cr, self._font_family, self._font_size, self._font_slant, + self._font_weight) + cairo_set_source_rgba(cr, unpack(self._color)) + for i, line in ipairs(self._lines) do + local y = self._baseline_offset + (i - 1) * self._line_height + self._write_fn(cr, self._x, y, line) + end +end + + +--- Draw a single line of changeable text. +-- Text line can be updated on each cycle via `set_text`. +-- @type TextLine +local TextLine = util.class(Text) +w.TextLine = TextLine + +--- @tparam table args table of options, see `Text:init` +function TextLine:init(args) + Text.init(self, args) + self.height = self._line_height +end + +--- Update the text line to be displayed. +-- @string text +function TextLine:set_text(text) + self._text = text +end + +function TextLine:render(cr) + ch.set_font(cr, self._font_family, self._font_size, self._font_slant, + self._font_weight) + cairo_set_source_rgba(cr, unpack(self._color)) + self._write_fn(cr, self._x, self._baseline_offset, self._text) +end + + +return w diff --git a/test/test_layout.lua b/test/test_layout.lua index 6acb529..64b7688 100644 --- a/test/test_layout.lua +++ b/test/test_layout.lua @@ -4,7 +4,8 @@ local script_dir = debug.getinfo(1, 'S').source:match("^@(.*/)") or "./" package.path = script_dir .. "../?.lua;" .. package.path -local widget = require('src/widget') +local core = require('src/widgets/core') +local text = require('src/widgets/text') -- minimal conky.config to run this script again once without opening a window local conkyrc = conky or {} @@ -55,7 +56,7 @@ end --- Assert that the output of a given renderer matches an existing image. -- @string name test name --- @tparam widget.Renderer renderer +-- @tparam core.Renderer renderer local function check_renderer(name, renderer) local out_path = TMP_PREFIX .. name .. ".png" local diff_path = TMP_PREFIX .. name .. "_diff.png" @@ -72,7 +73,7 @@ local frame_opts = { --- Mock Widget that does nothing but has a background plus border. local function dummy(args) - return widget.Frame(widget.Filler({width=args.width, height=args.height}), { + return core.Frame(core.Filler({width=args.width, height=args.height}), { background_color=args.background_color or frame_opts.background_color, border_color=args.border_color or frame_opts.border_color, border_width=args.border_width or frame_opts.border_width, @@ -87,41 +88,44 @@ end local test = {} function test.frame() - local inner = widget.Frame(widget.Filler{},{ + local inner = core.Frame(core.Filler{},{ margin = 2, background_color = {1, 0, 0, 0.8}, }) - local root = widget.Frame(inner, { + local root = core.Frame(inner, { margin = {10, 12, 16, 0}, padding = {0, 8, 12}, border_width = 12, border_color = {1, 1, 1, 1}, background_color = {0, 0, 0, 1}, }) - check_renderer("frame", widget.Renderer{root=root, width=100, height=100}) + check_renderer("frame", core.Renderer{root=root, width=100, height=100}) end function test.group() - local root = widget.Rows{ - widget.Frame(widget.Filler{}, frame_opts), - widget.Frame(widget.Filler{width=20}, frame_opts), - widget.Frame(widget.Filler{height=20}, frame_opts), - widget.Frame(widget.Filler{width=20, height=20}, frame_opts), + local root = core.Rows{ + core.Frame(core.Filler{}, frame_opts), + core.Frame(core.Filler{width=20}, frame_opts), + core.Frame(core.Filler{height=20}, frame_opts), + core.Frame(core.Filler{width=20, height=20}, frame_opts), } - check_renderer("group", widget.Renderer{root=root, width=40, height=100}) + check_renderer("group", core.Renderer{root=root, width=40, height=100}) end function test.columns() - local root = widget.Columns{ - widget.Frame(widget.Filler{}, frame_opts), - widget.Frame(widget.Filler{width=20}, frame_opts), - widget.Frame(widget.Filler{height=20}, frame_opts), - widget.Frame(widget.Filler{width=20, height=20}, frame_opts), + local root = core.Columns{ + core.Frame(core.Filler{}, frame_opts), + core.Frame(core.Filler{width=20}, frame_opts), + core.Frame(core.Filler{height=20}, frame_opts), + core.Frame(core.Filler{width=20, height=20}, frame_opts), } - check_renderer("columns", widget.Renderer{root=root, width=100, height=40}) + check_renderer("columns", core.Renderer{root=root, width=100, height=40}) end function test.text() + local Frame, Filler, Rows, Columns = core.Frame, core.Filler, + core.Rows, core.Columns + local StaticText = text.StaticText local LOREM_IPSUM = [[Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea @@ -130,40 +134,40 @@ esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.]] - local footer = widget.TextLine{align="center"} + local footer = text.TextLine{align="center"} footer:set_text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor in") - local root = widget.Frame(widget.Rows{ - widget.StaticText"Simple Text!", - widget.StaticText("This text should be centered.", {align="center"}), - widget.StaticText("Aligned to the right?", {align="right"}), + local root = Frame(Rows{ + StaticText"Simple Text!", + StaticText("This text should be centered.", {align="center"}), + StaticText("Aligned to the right?", {align="right"}), - widget.Filler{height=10}, + Filler{height=10}, -- paragraph with newlines - widget.StaticText(LOREM_IPSUM, { + StaticText(LOREM_IPSUM, { align="center", font_slant=CAIRO_FONT_SLANT_ITALIC, }), - widget.Filler{height=10}, + Filler{height=10}, - widget.StaticText("Large and Bold and Red", { + StaticText("Large and Bold and Red", { font_size=20, font_weight=CAIRO_FONT_WEIGHT_BOLD, color={1, 0, 0, 1}, }), - widget.StaticText("Small and Bold and Italic and Green", { + StaticText("Small and Bold and Italic and Green", { font_size=8, font_weight=CAIRO_FONT_WEIGHT_BOLD, font_slant=CAIRO_FONT_SLANT_ITALIC, color={0, 1, 0, 1}, }), - widget.Filler(), + Filler(), - widget.Frame(footer, { + Frame(footer, { border_sides={"top"}, border_width=1, border_color={1, 1, 1, .5}, @@ -172,7 +176,7 @@ laborum.]] padding=2, background_color={0, 0, 0, 1}, }) - check_renderer("text", widget.Renderer{root=root, width=400, height=200}) + check_renderer("text", core.Renderer{root=root, width=400, height=200}) end function test.complex_layout()