← Back to all plugins
×

Linear Issues Plugin

Display your Linear issues from current and past cycles on your TRMNL display

Quick Start

🚀 Easiest Method: Use the Recipe

The fastest way to get started is using the pre-configured recipe:

Add Linear Issues Recipe →

Manual Setup

Or follow these steps to set it up manually:

  1. Get your Linear API key from linear.app/settings/api
  2. Create a new plugin at usetrmnl.com/plugins
  3. Copy the configuration values below
  4. Copy and paste the markup template
  5. Save and activate your plugin

Plugin Configuration

Strategy
Polling
Polling URL
https://trml-plugins.vercel.app/api/linear-issues?linear_api_key={{ linear_api_key }}

The {{ linear_api_key }} variable will be automatically replaced with the user's API key from the form field

Polling Verb
GET
Polling Headers
(leave empty)
Polling Body
(leave empty)
Form Fields
- keyname: linear_api_key field_type: password name: Linear API Key description: Your personal Linear API key help_text: Get your API key from <a href="https://linear.app/settings/api" target="_blank">Linear Settings</a> - keyname: max_issues field_type: number name: Maximum Issues to Display description: How many issues should be shown default: 15 min: 5 max: 30 optional: true - keyname: author_info name: About This Plugin field_type: author_bio description: Linear Issues plugin displays your current and past cycle tasks. Built by Josh Sorenson, VP of Systems at Church Media Squad. github_url: https://github.com/joshsorenson/sqd-trml-plugins learn_more_url: https://trml-plugins.vercel.app/linear

Copy and paste this YAML into the Form Fields section in TRMNL

Remove Bleed Margin
No
Enable Dark Mode
No

Optional — choose based on your preference

Markup Template

Copy this template and paste it into TRMNL's markup editor:

{% comment %}
TRMNL Plugin: Linear Issues - Current Cycle & Overdue
Displays Linear issues assigned to you that are due in the current cycle or earlier
API Key: {{ trmnl.plugin_settings.custom_fields_values.linear_api_key }}
{% endcomment %}

<style>
  .priority-badge {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
  }
  
  .priority-1 { 
    background: #000;
    box-shadow: 0 0 0 2px rgba(0,0,0,0.2);
  }
  .priority-2 { 
    background: #666;
    box-shadow: 0 0 0 2px rgba(0,0,0,0.1);
  }
  .priority-3 { background: #999; }
  .priority-4 { background: #ccc; }
  
  .image.linear-icon {
    width: 1.5em;
    height: 1.5em;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' width='200' height='200' viewBox='0 0 100 100'%3E%3Cpath fill='%23000' d='M1.22541 61.5228c-.2225-.9485.90748-1.5459 1.59638-.857L39.3342 97.1782c.6889.6889.0915 1.8189-.857 1.5964C20.0515 94.4522 5.54779 79.9485 1.22541 61.5228ZM.00189135 46.8891c-.01764375.2833.08887215.5599.28957165.7606L52.3503 99.7085c.2007.2007.4773.3075.7606.2896 2.3692-.1476 4.6938-.46 6.9624-.9259.7645-.157 1.0301-1.0963.4782-1.6481L2.57595 39.4485c-.55186-.5519-1.49117-.2863-1.648174.4782-.465915 2.2686-.77832 4.5932-.92588465 6.9624ZM4.21093 29.7054c-.16649.3738-.08169.8106.20765 1.1l64.77602 64.776c.2894.2894.7262.3742 1.1.2077 1.7861-.7956 3.5171-1.6927 5.1855-2.684.5521-.328.6373-1.0867.1832-1.5407L8.43566 24.3367c-.45409-.4541-1.21271-.3689-1.54074.1832-.99132 1.6684-1.88843 3.3994-2.68399 5.1855ZM12.6587 18.074c-.3701-.3701-.393-.9637-.0443-1.3541C21.7795 6.45931 35.1114 0 49.9519 0 77.5927 0 100 22.4073 100 50.0481c0 14.8405-6.4593 28.1724-16.7199 37.3375-.3903.3487-.984.3258-1.3542-.0443L12.6587 18.074Z'/%3E%3C/svg%3E");
    background-size: contain;
    background-repeat: no-repeat;
    vertical-align: middle;
  }
</style>

<style>
  :is(.view--half_vertical, .view--quadrant, .view--half_horizontal) .full-table-view {
    display: none;
  }
  
  :is(.view--full) .compact-list-view {
    display: none;
  }
  
  /* Increase font sizes */
  .full-table-view .title--small,
  .full-table-view .label--small {
    font-size: 1.15em;
  }
  
  .compact-list-view .title--small,
  .compact-list-view .label--small {
    font-size: 1.15em;
  }
  
  .title_bar .title,
  .title_bar .instance {
    font-size: 1.15em;
  }
  
  /* Half-screen optimizations */
  .compact-list-view {
    display: flex;
    flex-direction: column;
    height: 100%;
    justify-content: space-between;
  }
  
  .compact-list-view .layout {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
  }
  
  .compact-list-view .item {
    padding: 0.75em 0;
    flex-shrink: 0;
  }
  
  .compact-list-view .item .content {
    flex: 1;
    min-width: 0;
  }
  
  .compact-list-view .item .title {
    word-break: break-word;
    overflow-wrap: break-word;
  }
  
  .compact-list-view .meta {
    flex-shrink: 0;
  }
  
  /* Limit items by view size */
  .view--quadrant .compact-list-view .item:nth-child(n+3) {
    display: none;
  }
  
  :is(.view--half_vertical, .view--half_horizontal) .compact-list-view .item:nth-child(n+6) {
    display: none;
  }
</style>

{% comment %} Full Table Layout {% endcomment %}
<div class="layout layout--top full-table-view">
  <table class="table" id="linear-issues">
    <thead>
      <tr>
        <th><span class="title title--small">ID</span></th>
        <th><span class="title title--small">Title</span></th>
        <th><span class="title title--small">Status</span></th>
        <th><span class="title title--small">Cycle</span></th>
      </tr>
    </thead>
    <tbody>
      {% assign max_issues = trmnl.plugin_settings.custom_fields_values.max_issues | default: 15 %}
      {% for issue in issues limit: max_issues %}
      <tr>
        <td>
          <span class="label label--small issue-id">
            {% if issue.priority > 0 and issue.priority < 3 %}
              <span class="priority-badge priority-{{ issue.priority }} mr--xsmall"></span>
            {% endif %}
            {{ issue.identifier }}
          </span>
        </td>
        <td>
          <span class="label label--small clamp clamp--2 issue-title">{{ issue.title }}</span>
        </td>
        <td>
          <span class="label label--small issue-status">{{ issue.status }}</span>
        </td>
        <td class="text--center">
          <span class="label label--small cycle-info">
            {% if issue.cycleNumber %}
              #{{ issue.cycleNumber }}
              {% if issue.cycleStatus == 'current' %}
                <small>●</small>
              {% elsif issue.cycleStatus == 'past' %}
                <small>◄</small>
              {% elsif issue.cycleStatus == 'future' %}
                <small>►</small>
              {% endif %}
            {% else %}
              -
            {% endif %}
          </span>
        </td>
      </tr>
      {% endfor %}
    </tbody>
  </table>
</div>

{% comment %} Compact List Layout (Half Vertical / Half Horizontal / Quadrant) {% endcomment %}
<div class="compact-list-view">
<div class="layout layout--fill">
  {% assign max_issues_compact = trmnl.plugin_settings.custom_fields_values.max_issues | default: 10 %}
  {% for issue in issues limit: max_issues_compact %}
    <div class="item">
      <div class="meta">
        {% if issue.priority > 0 and issue.priority < 3 %}
          <span class="priority-badge priority-{{ issue.priority }}"></span>
        {% else %}
          <span class="index">{{ forloop.index }}</span>
        {% endif %}
      </div>
      <div class="content">
        <div class="flex gap--xsmall" style="flex-wrap: wrap;">
          <span class="label label--small" style="font-weight: 600;">{{ issue.identifier }}</span>
          <span class="label label--small clamp clamp--2" style="flex: 1; min-width: 0;">{{ issue.title }}</span>
        </div>
        <div class="flex gap--xsmall" style="margin-top: 0.25em;">
          <span class="label label--small">{{ issue.status }}</span>
          {% if issue.cycleNumber %}
            <span class="label label--small">
              #{{ issue.cycleNumber }}
              {% if issue.cycleStatus == 'current' %}
                <small>●</small>
              {% elsif issue.cycleStatus == 'past' %}
                <small>◄</small>
              {% elsif issue.cycleStatus == 'future' %}
                <small>►</small>
              {% endif %}
            </span>
          {% endif %}
        </div>
      </div>
    </div>
  {% endfor %}
</div>
</div>

<div class="title_bar">
  <span class="image linear-icon image-stroke"></span>
  <span class="title">{{ trmnl.plugin_settings.instance_name }}</span>
  <span class="instance">{{ issues | size }} Issues{% if current_cycle %} • Cycle #{{ current_cycle }}{% endif %}</span>
</div>

<script>
  var issues_table = document.querySelector("#linear-issues");
  if (issues_table.clientHeight >= issues_table.parentNode.clientHeight) {
    issues_table.classList.add("table--condensed");
  }
</script>

Features