[Sync] Lua Hyprland chain, licence cleanup
This commit is contained in:
1 parent
93b4f695ce
commit
47166d877f
183 files changed
+5618
-5839
No files matched your search
@@ -51,7 +51,7 @@ clients:
|
||||
- name: GLM-4.7-Flash-UD-Q3_K_XL
|
||||
max_input_tokens: 22000 # ctx 24576 · ~28 t/s — fast coder (3-bit)
|
||||
- name: Ornith-1.5-35B-A3B-IQ3_XXS
|
||||
max_input_tokens: 22000 # ctx 24576 · ~32 t/s — agentic coder, thinking
|
||||
max_input_tokens: 30000 # ctx 32768 · ~32 t/s — agentic coder, thinking
|
||||
supports_vision: true
|
||||
- name: gpt-oss-20b
|
||||
max_input_tokens: 62000 # ctx 65536 · ~38 t/s — fast reasoning + tools
|
||||
|
||||
@@ -1,663 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Name: One Half Dark
|
||||
Author: Son A. Pham <sp@sonpham.me>
|
||||
Url: https://github.com/sonph/onehalf
|
||||
License: The MIT License (MIT)
|
||||
|
||||
A dark Sublime Text color scheme based on Atom's One. See
|
||||
github.com/sonph/onehalf for installation instructions, a light color
|
||||
scheme, and versions for other editors/terminals such as Vim or iTerm.
|
||||
|
||||
Red: #e06c75
|
||||
Green: #98c379
|
||||
Yellow: #e5c07b
|
||||
Blue: #61afef
|
||||
Purple: #c678dd
|
||||
Cyan: #56b6c2
|
||||
White: #dcdfe4
|
||||
Black: #282c34
|
||||
Fg: #dcdfe4
|
||||
Bg: #282c34
|
||||
Comment: #5c6370
|
||||
Gutter foreground: #919baa
|
||||
Gutter background: #282c34
|
||||
Selection: #474e5d
|
||||
-->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>OneHalfLight</string>
|
||||
<key>semanticClass</key>
|
||||
<string>theme.dark.one_half_dark</string>
|
||||
<key>uuid</key>
|
||||
<string></string>
|
||||
<key>colorSpaceName</key>
|
||||
<string>sRGB</string>
|
||||
<key>author</key>
|
||||
<string>Son A. Pham <sp@sonpham.me></string>
|
||||
<key>comment</key>
|
||||
<string>A dark iTerm color scheme based on Atom's One. See github.com/sonph/onehalf for installation instructions, a light color scheme, and versions for other editors/terminals such as (Neo)Vim and Sublime Text.</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>fontStyle</key>
|
||||
<string></string>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
<key>background</key>
|
||||
<string>#282c34</string>
|
||||
<key>bracketsOptions</key>
|
||||
<string>underline</string>
|
||||
<key>caret</key>
|
||||
<string>#a3b3cc</string>
|
||||
<key>gutter</key>
|
||||
<string>#282c34</string>
|
||||
<key>gutterForeground</key>
|
||||
<string>#919baa</string>
|
||||
<key>invisibles</key>
|
||||
<string>#5c6370</string>
|
||||
<key>lineHighlight</key>
|
||||
<string>#313640</string>
|
||||
<key>selection</key>
|
||||
<string>#474e5d</string>
|
||||
<key>selectionBorder</key>
|
||||
<string>#474e5d</string>
|
||||
<key>tagsForeground</key>
|
||||
<string></string>
|
||||
<key>tagsOptions</key>
|
||||
<string>stippled_underline</string>
|
||||
<key>bracketContentsOptions</key>
|
||||
<string>underline</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Comments</string>
|
||||
<key>scope</key>
|
||||
<string>comment</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#5c6370</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Text</string>
|
||||
<key>scope</key>
|
||||
<string>variable.parameter.function</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Delimiters</string>
|
||||
<key>scope</key>
|
||||
<string>none</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Operators</string>
|
||||
<key>scope</key>
|
||||
<string>keyword.operator</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Keywords</string>
|
||||
<key>scope</key>
|
||||
<string>keyword</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#c678dd</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Variables</string>
|
||||
<key>scope</key>
|
||||
<string>variable</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Functions</string>
|
||||
<key>scope</key>
|
||||
<string>entity.name.function, meta.require, support.function.any-method</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#61afef</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Classes</string>
|
||||
<key>scope</key>
|
||||
<string>support.class, entity.name.class, entity.name.type.class</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Classes</string>
|
||||
<key>scope</key>
|
||||
<string>meta.class</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Methods</string>
|
||||
<key>scope</key>
|
||||
<string>keyword.other.special-method</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#61afef</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Storage</string>
|
||||
<key>scope</key>
|
||||
<string>storage</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#c678dd</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Support</string>
|
||||
<key>scope</key>
|
||||
<string>support.function</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#61afef</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Strings, Inherited Class</string>
|
||||
<key>scope</key>
|
||||
<string>string</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#98c379</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Integers</string>
|
||||
<key>scope</key>
|
||||
<string>constant.numeric</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Floats</string>
|
||||
<key>scope</key>
|
||||
<string>none</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Boolean</string>
|
||||
<key>scope</key>
|
||||
<string>none</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Constants</string>
|
||||
<key>scope</key>
|
||||
<string>constant</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>HTML: Tags</string>
|
||||
<key>scope</key>
|
||||
<string>entity.name.tag</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>HTML: Tag attributes</string>
|
||||
<key>scope</key>
|
||||
<string>entity.other.attribute-name</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Attribute IDs</string>
|
||||
<key>scope</key>
|
||||
<string>entity.other.attribute-name.id, punctuation.definition.entity</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Selector</string>
|
||||
<key>scope</key>
|
||||
<string>meta.selector</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#c678dd</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markdown: Headings</string>
|
||||
<key>scope</key>
|
||||
<string>markup.heading punctuation.definition.heading, entity.name.section</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>fontStyle</key>
|
||||
<string></string>
|
||||
<key>foreground</key>
|
||||
<string>#61afef</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markdown: Bold</string>
|
||||
<key>scope</key>
|
||||
<string>markup.bold, punctuation.definition.bold</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#c678dd</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markdown: Italic</string>
|
||||
<key>scope</key>
|
||||
<string>markup.italic, punctuation.definition.italic</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#c678dd</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markdown: Code</string>
|
||||
<key>scope</key>
|
||||
<string>markup.raw.inline</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#98c379</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markdown: Link Text</string>
|
||||
<key>scope</key>
|
||||
<string>string.other.link, punctuation.definition.string.end.markdown</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markdown: Link Url</string>
|
||||
<key>scope</key>
|
||||
<string>meta.link</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#98c379</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markdown: Lists</string>
|
||||
<key>scope</key>
|
||||
<string>markup.list</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markdown: Quotes</string>
|
||||
<key>scope</key>
|
||||
<string>markup.quote</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#98c379</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Java Source</string>
|
||||
<key>scope</key>
|
||||
<string>source.java meta.class.java meta.method.java</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Java Class Body</string>
|
||||
<key>scope</key>
|
||||
<string>source.java meta.class.java meta.class.body.java</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: Function Arguments</string>
|
||||
<key>scope</key>
|
||||
<string>source.js meta.function.js variable.parameter.function.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: New Variables</string>
|
||||
<key>scope</key>
|
||||
<string>source.js variable.other.readwrite.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: Variables</string>
|
||||
<key>scope</key>
|
||||
<string>source.js variable.other.object.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: Variables in Function Calls</string>
|
||||
<key>scope</key>
|
||||
<string>source.js meta.function-call.method.js variable.other.readwrite.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: New Block Variables</string>
|
||||
<key>scope</key>
|
||||
<string>source.js meta.block.js variable.other.readwrite.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: Block Variables</string>
|
||||
<key>scope</key>
|
||||
<string>source.js meta.block.js variable.other.object.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: Block Variables in Function Calls</string>
|
||||
<key>scope</key>
|
||||
<string>source.js meta.block.js meta.function-call.method.js variable.other.readwrite.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: Function Calls</string>
|
||||
<key>scope</key>
|
||||
<string>source.js meta.function-call.method.js variable.function.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: Properties</string>
|
||||
<key>scope</key>
|
||||
<string>source.js meta.property.object.js entity.name.function.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#61afef</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Javascript: Prototypes</string>
|
||||
<key>scope</key>
|
||||
<string>source.js support.constant.prototype.js</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Separator</string>
|
||||
<key>scope</key>
|
||||
<string>meta.separator</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>background</key>
|
||||
<string></string>
|
||||
<key>foreground</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Inserted</string>
|
||||
<key>scope</key>
|
||||
<string>markup.inserted</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#98c379</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Deleted</string>
|
||||
<key>scope</key>
|
||||
<string>markup.deleted</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Changed</string>
|
||||
<key>scope</key>
|
||||
<string>markup.changed</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e5c07b</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Regular Expressions</string>
|
||||
<key>scope</key>
|
||||
<string>string.regexp</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#98c379</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Escape Characters</string>
|
||||
<key>scope</key>
|
||||
<string>constant.character.escape</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#56b6c2</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Embedded</string>
|
||||
<key>scope</key>
|
||||
<string>punctuation.section.embedded, variable.interpolation</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Illegal</string>
|
||||
<key>scope</key>
|
||||
<string>invalid.illegal</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>background</key>
|
||||
<string>#e06c75</string>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Broken</string>
|
||||
<key>scope</key>
|
||||
<string>invalid.broken</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>background</key>
|
||||
<string>#e5c07b</string>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Deprecated</string>
|
||||
<key>scope</key>
|
||||
<string>invalid.deprecated</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>background</key>
|
||||
<string>#e5c07b</string>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Unimplemented</string>
|
||||
<key>scope</key>
|
||||
<string>invalid.unimplemented</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>background</key>
|
||||
<string>#c678dd</string>
|
||||
<key>foreground</key>
|
||||
<string>#dcdfe4</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# Specify desired highlighting theme (e.g. "TwoDark"). Run `bat --list-themes`
|
||||
# for a list of all available themes
|
||||
# Custom theme lives in ./themes/ — run `bat cache --build` after changing it
|
||||
# OneHalfDark is built into bat — no custom themes are tracked, so no `bat cache --build` step
|
||||
--theme="OneHalfDark"
|
||||
--style="numbers,changes,header,snip"
|
||||
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>One Dark (NvChad base46)</string>
|
||||
<key>uuid</key>
|
||||
<string>3c1e2b84-9d5f-4a7c-8b2e-6f4a1d9c7e5b</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>background</key>
|
||||
<string>#1e222a</string>
|
||||
<key>foreground</key>
|
||||
<string>#abb2bf</string>
|
||||
<key>caret</key>
|
||||
<string>#abb2bf</string>
|
||||
<key>selection</key>
|
||||
<string>#3e4451</string>
|
||||
<key>lineHighlight</key>
|
||||
<string>#252931</string>
|
||||
<key>gutterForeground</key>
|
||||
<string>#545862</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Comment</string>
|
||||
<key>scope</key>
|
||||
<string>comment, punctuation.definition.comment</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#6f737b</string>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>String</string>
|
||||
<key>scope</key>
|
||||
<string>string</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#98c379</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Number, constant</string>
|
||||
<key>scope</key>
|
||||
<string>constant.numeric, constant.language, constant.other</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#d19a66</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Escape character</string>
|
||||
<key>scope</key>
|
||||
<string>constant.character.escape</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#a3b8ef</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Keyword, storage</string>
|
||||
<key>scope</key>
|
||||
<string>keyword, storage</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#c882e7</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Operator</string>
|
||||
<key>scope</key>
|
||||
<string>keyword.operator</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#abb2bf</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Function</string>
|
||||
<key>scope</key>
|
||||
<string>entity.name.function, support.function, meta.function-call.generic</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#61afef</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Class, type</string>
|
||||
<key>scope</key>
|
||||
<string>entity.name.class, entity.name.type, entity.name.struct, entity.name.enum, support.type, support.class</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e7c787</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Variable</string>
|
||||
<key>scope</key>
|
||||
<string>variable, variable.other</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Function parameter</string>
|
||||
<key>scope</key>
|
||||
<string>variable.parameter</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Attribute</string>
|
||||
<key>scope</key>
|
||||
<string>entity.other.attribute-name</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#d19a66</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Tag</string>
|
||||
<key>scope</key>
|
||||
<string>entity.name.tag, meta.tag</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup heading</string>
|
||||
<key>scope</key>
|
||||
<string>markup.heading, entity.name.section</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
<key>fontStyle</key>
|
||||
<string>bold</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup bold</string>
|
||||
<key>scope</key>
|
||||
<string>markup.bold</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>fontStyle</key>
|
||||
<string>bold</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup italic</string>
|
||||
<key>scope</key>
|
||||
<string>markup.italic</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup link</string>
|
||||
<key>scope</key>
|
||||
<string>markup.underline.link, string.other.link</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#61afef</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup quote</string>
|
||||
<key>scope</key>
|
||||
<string>markup.quote</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#6f737b</string>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Diff inserted</string>
|
||||
<key>scope</key>
|
||||
<string>markup.inserted</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#98c379</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Diff deleted</string>
|
||||
<key>scope</key>
|
||||
<string>markup.deleted</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#e06c75</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Invalid</string>
|
||||
<key>scope</key>
|
||||
<string>invalid, invalid.illegal</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#ffffff</string>
|
||||
<key>background</key>
|
||||
<string>#be5046</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,262 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Synthwave 84</string>
|
||||
<key>uuid</key>
|
||||
<string>7b2c3a10-4b6e-4d8f-9c1a-2e5f8d7a9b3c</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>background</key>
|
||||
<string>#262335</string>
|
||||
<key>foreground</key>
|
||||
<string>#ffffff</string>
|
||||
<key>caret</key>
|
||||
<string>#f97e72</string>
|
||||
<key>selection</key>
|
||||
<string>#463465</string>
|
||||
<key>lineHighlight</key>
|
||||
<string>#2a2139</string>
|
||||
<key>gutterForeground</key>
|
||||
<string>#848bbd</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Comment</string>
|
||||
<key>scope</key>
|
||||
<string>comment, punctuation.definition.comment</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#848bbd</string>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>String</string>
|
||||
<key>scope</key>
|
||||
<string>string</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#ff8b39</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Number, constant</string>
|
||||
<key>scope</key>
|
||||
<string>constant.numeric, constant.language, constant.other</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#f97e72</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Escape character</string>
|
||||
<key>scope</key>
|
||||
<string>constant.character.escape</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#36f9f6</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Keyword, storage</string>
|
||||
<key>scope</key>
|
||||
<string>keyword, storage, keyword.operator</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#fede5d</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Storage type</string>
|
||||
<key>scope</key>
|
||||
<string>storage.type, storage.modifier</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#fede5d</string>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Function</string>
|
||||
<key>scope</key>
|
||||
<string>entity.name.function, support.function, meta.function-call.generic</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#36f9f6</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Class, type</string>
|
||||
<key>scope</key>
|
||||
<string>entity.name.class, entity.name.type, entity.name.struct, entity.name.enum, support.type, support.class</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#fe4450</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Variable</string>
|
||||
<key>scope</key>
|
||||
<string>variable, variable.other</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#ff7edb</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Function parameter</string>
|
||||
<key>scope</key>
|
||||
<string>variable.parameter</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#ff7edb</string>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Attribute</string>
|
||||
<key>scope</key>
|
||||
<string>entity.other.attribute-name</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#fede5d</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Tag</string>
|
||||
<key>scope</key>
|
||||
<string>entity.name.tag, meta.tag</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#72f1b8</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup heading</string>
|
||||
<key>scope</key>
|
||||
<string>markup.heading, entity.name.section</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#fe4450</string>
|
||||
<key>fontStyle</key>
|
||||
<string>bold</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup bold</string>
|
||||
<key>scope</key>
|
||||
<string>markup.bold</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>fontStyle</key>
|
||||
<string>bold</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup italic</string>
|
||||
<key>scope</key>
|
||||
<string>markup.italic</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup link</string>
|
||||
<key>scope</key>
|
||||
<string>markup.underline.link, string.other.link</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#36f9f6</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Markup quote</string>
|
||||
<key>scope</key>
|
||||
<string>markup.quote</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#848bbd</string>
|
||||
<key>fontStyle</key>
|
||||
<string>italic</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Diff inserted</string>
|
||||
<key>scope</key>
|
||||
<string>markup.inserted</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#72f1b8</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Diff deleted</string>
|
||||
<key>scope</key>
|
||||
<string>markup.deleted</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#fe4450</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>Invalid</string>
|
||||
<key>scope</key>
|
||||
<string>invalid, invalid.illegal</string>
|
||||
<key>settings</key>
|
||||
<dict>
|
||||
<key>foreground</key>
|
||||
<string>#ffffff</string>
|
||||
<key>background</key>
|
||||
<string>#fe4450</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
+4
-294
@@ -1,241 +1,13 @@
|
||||
## Configuration file for CAVA.
|
||||
# Remove the ; to change parameters.
|
||||
## cava — only this repo's own settings. Every key left out falls back to cava's built-in
|
||||
## default; cava documents them all in the config it writes to an empty ~/.config/cava/config,
|
||||
## and it regenerates its own shaders/ and themes/ there on first run (so the repo ships none
|
||||
## of cava's files).
|
||||
|
||||
[general]
|
||||
|
||||
# Smoothing mode. Can be 'normal', 'scientific' or 'waves'. DEPRECATED as of 0.6.0
|
||||
# mode = scientific # deprecated since cava 0.6.0, ignored
|
||||
|
||||
# Accepts only non-negative values.
|
||||
framerate = 60
|
||||
|
||||
# 'autosens' will attempt to decrease sensitivity if the bars peak. 1 = on, 0 = off
|
||||
# new as of 0.6.0 autosens of low values (dynamic range)
|
||||
# 'overshoot' allows bars to overshoot (in % of terminal height) without initiating autosens. DEPRECATED as of 0.6.0
|
||||
autosens = 1
|
||||
; overshoot = 20
|
||||
|
||||
# Manual sensitivity in %. If autosens is enabled, this will only be the initial value.
|
||||
# 200 means double height. Accepts only non-negative values.
|
||||
; sensitivity = 100
|
||||
|
||||
# The number of bars (0-512). 0 sets it to auto (fill up console).
|
||||
# Bars' width and space between bars in number of characters.
|
||||
; bars = 0
|
||||
; bar_width = 2
|
||||
; bar_spacing = 1
|
||||
# bar_height is only used for output in "noritake" format
|
||||
; bar_height = 32
|
||||
|
||||
# For SDL width and space between bars is in pixels, defaults are:
|
||||
; bar_width = 20
|
||||
; bar_spacing = 5
|
||||
|
||||
# sdl_glsl have these default values, they are only used to calculate max number of bars.
|
||||
; bar_width = 1
|
||||
; bar_spacing = 0
|
||||
|
||||
# ceter bars in terminal, if there is space.
|
||||
; center_align = 1
|
||||
|
||||
# max height of bars in terminal, in percent of terminal height.
|
||||
; max_height = 100
|
||||
|
||||
|
||||
# Lower and higher cutoff frequencies for lowest and highest bars
|
||||
# the bandwidth of the visualizer.
|
||||
# Note: there is a minimum total bandwidth of 43Mhz x number of bars.
|
||||
# Cava will automatically increase the higher cutoff if a too low band is specified.
|
||||
; lower_cutoff_freq = 50
|
||||
; higher_cutoff_freq = 10000
|
||||
|
||||
|
||||
# Seconds with no input before cava goes to sleep mode. Cava will not perform FFT or drawing and
|
||||
# only check for input once per second. Cava will wake up once input is detected. 0 = disable.
|
||||
; sleep_timer = 0
|
||||
|
||||
|
||||
[input]
|
||||
|
||||
# Audio capturing method. Possible methods are: 'fifo', 'portaudio', 'pipewire', 'alsa', 'pulse', 'sndio', 'oss', 'jack' or 'shmem'
|
||||
# Defaults to 'oss', 'pipewire', 'sndio', 'jack', 'pulse', 'alsa', 'portaudio' or 'fifo', in that order, dependent on what support cava was built with.
|
||||
# On Mac it defaults to 'portaudio' or 'fifo'
|
||||
# On windows this is automatic and no input settings are needed.
|
||||
#
|
||||
# All input methods uses the same config variable 'source'
|
||||
# to define where it should get the audio.
|
||||
#
|
||||
# For pulseaudio and pipewire 'source' will be the source. Default: 'auto', which uses the monitor source of the default sink
|
||||
# (all pulseaudio sinks(outputs) have 'monitor' sources(inputs) associated with them).
|
||||
#
|
||||
# For pipewire 'source' will be the object name or object.serial of the device to capture from.
|
||||
# Both input and output devices are supported. To capture the monitor source of a sink node, append '.monitor' to the sink's object name.
|
||||
#
|
||||
# For alsa 'source' will be the capture device.
|
||||
# For fifo 'source' will be the path to fifo-file.
|
||||
# For shmem 'source' will be /squeezelite-AA:BB:CC:DD:EE:FF where 'AA:BB:CC:DD:EE:FF' will be squeezelite's MAC address
|
||||
#
|
||||
# For sndio 'source' will be a raw recording audio descriptor or a monitoring sub-device, e.g. 'rsnd/2' or 'snd/1'. Default: 'default'.
|
||||
# README.md contains further information on how to setup CAVA for sndio.
|
||||
#
|
||||
# For oss 'source' will be the path to a audio device, e.g. '/dev/dsp2'. Default: '/dev/dsp', i.e. the default audio device.
|
||||
# README.md contains further information on how to setup CAVA for OSS on FreeBSD.
|
||||
#
|
||||
# For jack 'source' will be the name of the JACK server to connect to, e.g. 'foobar'. Default: 'default'.
|
||||
# README.md contains further information on how to setup CAVA for JACK.
|
||||
#
|
||||
; method = pulse
|
||||
; source = auto
|
||||
|
||||
; method = pipewire
|
||||
; source = auto
|
||||
|
||||
; method = alsa
|
||||
; source = hw:Loopback,1
|
||||
|
||||
; method = fifo
|
||||
; source = /tmp/mpd.fifo
|
||||
|
||||
; method = shmem
|
||||
; source = /squeezelite-AA:BB:CC:DD:EE:FF
|
||||
|
||||
; method = portaudio
|
||||
; source = auto
|
||||
|
||||
; method = sndio
|
||||
; source = default
|
||||
|
||||
; method = oss
|
||||
; source = /dev/dsp
|
||||
|
||||
; method = jack
|
||||
; source = default
|
||||
|
||||
# The options 'sample_rate', 'sample_bits', 'channels' and 'autoconnect' can be configured for some input methods:
|
||||
# sample_rate: fifo, pipewire, sndio, oss
|
||||
# sample_bits: fifo, pipewire, sndio, oss
|
||||
# channels: sndio, oss, jack
|
||||
# autoconnect: jack
|
||||
# Other methods ignore these settings.
|
||||
#
|
||||
# For 'sndio' and 'oss' they are only preferred values, i.e. if the values are not supported
|
||||
# by the chosen audio device, the device will use other supported values instead.
|
||||
# Example: 48000, 32 and 2, but the device only supports 44100, 16 and 1, then it
|
||||
# will use 44100, 16 and 1.
|
||||
#
|
||||
; sample_rate = 44100
|
||||
; sample_bits = 16
|
||||
; channels = 2
|
||||
; autoconnect = 2
|
||||
|
||||
|
||||
[output]
|
||||
|
||||
# Output method. Can be 'ncurses', 'noncurses', 'raw', 'noritake', 'sdl'
|
||||
# or 'sdl_glsl'.
|
||||
# 'noncurses' (default) uses a buffer and cursor movements to only print
|
||||
# changes from frame to frame in the terminal. Uses less resources and is less
|
||||
# prone to tearing (vsync issues) than 'ncurses'.
|
||||
#
|
||||
# 'raw' is an 8 or 16 bit (configurable via the 'bit_format' option) data
|
||||
# stream of the bar heights that can be used to send to other applications.
|
||||
# 'raw' defaults to 1024 bars stereo (512 bars mono), which can be adjusted in the 'bars' option above.
|
||||
#
|
||||
# 'noritake' outputs a bitmap in the format expected by a Noritake VFD display
|
||||
# in graphic mode. It only support the 3000 series graphical VFDs for now.
|
||||
#
|
||||
# 'sdl' uses the Simple DirectMedia Layer to render in a graphical context.
|
||||
# 'sdl_glsl' uses SDL to create an OpenGL context. Write your own shaders or
|
||||
# use one of the predefined ones.
|
||||
; method = noncurses
|
||||
|
||||
# Orientation of the visualization. Can be 'bottom', 'top', 'left', 'right' or
|
||||
# 'horizontal'. Default is 'bottom'. 'left and 'right' are only supported on sdl
|
||||
# and ncruses output. 'horizontal' (bars go up and down from center) is only supported
|
||||
# on noncurses output.
|
||||
# Note: many fonts have weird or missing glyphs for characters used in orientations
|
||||
# other than 'bottom', which can make output not look right.
|
||||
; orientation = bottom
|
||||
|
||||
# Visual channels. Can be 'stereo' or 'mono'.
|
||||
# 'stereo' mirrors both channels with low frequencies in center.
|
||||
# 'mono' outputs left to right lowest to highest frequencies.
|
||||
# 'mono_option' set mono to either take input from 'left', 'right' or 'average'.
|
||||
# set 'reverse' to 1 to display frequencies the other way around.
|
||||
; channels = stereo
|
||||
; mono_option = average
|
||||
; reverse = 0
|
||||
|
||||
# Raw output target.
|
||||
# On Linux, a fifo will be created if target does not exist.
|
||||
# On Windows, a named pipe will be created if target does not exist.
|
||||
; raw_target = /dev/stdout
|
||||
|
||||
# Raw data format. Can be 'binary' or 'ascii'.
|
||||
; data_format = binary
|
||||
|
||||
# Binary bit format, can be '8bit' (0-255) or '16bit' (0-65530).
|
||||
; bit_format = 16bit
|
||||
|
||||
# Ascii max value. In 'ascii' mode range will run from 0 to value specified here
|
||||
; ascii_max_range = 1000
|
||||
|
||||
# Ascii delimiters. In ascii format each bar and frame is separated by a delimiters.
|
||||
# Use decimal value in ascii table (i.e. 59 = ';' and 10 = '\n' (line feed)).
|
||||
; bar_delimiter = 59
|
||||
; frame_delimiter = 10
|
||||
|
||||
# sdl window size and position. -1,-1 is centered.
|
||||
; sdl_width = 1024
|
||||
; sdl_height = 512
|
||||
; sdl_x = -1
|
||||
; sdl_y= -1
|
||||
; sdl_full_screen = 0
|
||||
|
||||
# set label on bars on the x-axis. Can be 'frequency' or 'none'. Default: 'none'
|
||||
# 'frequency' displays the lower cut off frequency of the bar above.
|
||||
# Only supported on ncurses and noncurses output.
|
||||
; xaxis = none
|
||||
|
||||
# enable synchronized sync. 1 = on, 0 = off
|
||||
# removes flickering in alacritty terminal emulator.
|
||||
# defaults to off since the behaviour in other terminal emulators is unknown
|
||||
; synchronized_sync = 0
|
||||
|
||||
# Shaders for sdl_glsl, located in $HOME/.config/cava/shaders
|
||||
; vertex_shader = pass_through.vert
|
||||
; fragment_shader = bar_spectrum.frag
|
||||
|
||||
; for glsl output mode, keep rendering even if no audio
|
||||
; continuous_rendering = 0
|
||||
|
||||
# disable console blank (screen saver) in tty
|
||||
# (Not supported on FreeBSD)
|
||||
; disable_blanking = 0
|
||||
|
||||
# show a flat bar at the bottom of the screen when idle, 1 = on, 0 = off
|
||||
; show_idle_bar_heads = 1
|
||||
|
||||
# show waveform instead of frequency spectrum, 1 = on, 0 = off
|
||||
; waveform = 0
|
||||
|
||||
[color]
|
||||
|
||||
# Colors can be one of seven predefined: black, blue, cyan, green, magenta, red, white, yellow.
|
||||
# Or defined by hex code '#xxxxxx' (hex code must be within ''). User defined colors requires
|
||||
# a terminal that can change color definitions such as Gnome-terminal or rxvt.
|
||||
# default is to keep current terminal color
|
||||
; background = default
|
||||
; foreground = default
|
||||
|
||||
# SDL and sdl_glsl only support hex code colors, these are the default:
|
||||
; background = '#111111'
|
||||
; foreground = '#D34b11'
|
||||
# ; foreground = '#D34b11'
|
||||
|
||||
# Gradient mode, only hex defined colors are supported,
|
||||
# background must also be defined in hex or remain commented out. 1 = on, 0 = off.
|
||||
# You can define as many as 8 different colors. They range from bottom to top of screen
|
||||
gradient = 1
|
||||
gradient_color_1 = '#d34b11'
|
||||
gradient_color_2 = '#cc5933'
|
||||
@@ -245,65 +17,3 @@ gradient_color_5 = '#cccc33'
|
||||
gradient_color_6 = '#a6cc33'
|
||||
gradient_color_7 = '#80cc33'
|
||||
gradient_color_8 = '#59cc33'
|
||||
|
||||
# Horizontal is only supported on noncurses output.
|
||||
# Only one color will be calculated per bar.
|
||||
; horizontal_gradient = 1
|
||||
; horizontal_gradient_color_1 = '#c45161'
|
||||
; horizontal_gradient_color_2 = '#e094a0'
|
||||
; horizontal_gradient_color_3 = '#f2b6c0'
|
||||
; horizontal_gradient_color_4 = '#f2dde1'
|
||||
; horizontal_gradient_color_5 = '#cbc7d8'
|
||||
; horizontal_gradient_color_6 = '#8db7d2'
|
||||
; horizontal_gradient_color_7 = '#5e62a9'
|
||||
; horizontal_gradient_color_8 = '#434279'
|
||||
|
||||
|
||||
# If both vertical and horizontal gradient is enabled, vertical will be blended in this direction.
|
||||
# Can be 'up', 'down', 'left' or 'right'. 'up' means the vertical gradient will be blended in from
|
||||
# bottom to top. I.e. the bottom will be only the horizontal
|
||||
# and top will be only the color of the vertical gradient.
|
||||
; blend_direction = 'up'
|
||||
|
||||
# use theme file instead of defining colors in this file
|
||||
# themes are located in $HOME/.config/cava/themes
|
||||
; theme = 'none'
|
||||
|
||||
|
||||
[smoothing]
|
||||
|
||||
# Percentage value for integral smoothing. Takes values from 0 - 100.
|
||||
# Higher values means smoother, but less precise. 0 to disable.
|
||||
# DEPRECATED as of 0.8.0, use noise_reduction instead
|
||||
; integral = 77
|
||||
|
||||
# Disables or enables the so-called "Monstercat smoothing" with or without "waves". Set to 0 to disable.
|
||||
; monstercat = 0
|
||||
; waves = 0
|
||||
|
||||
# Set gravity percentage for "drop off". Higher values means bars will drop faster.
|
||||
# Accepts only non-negative values. 50 means half gravity, 200 means double. Set to 0 to disable "drop off".
|
||||
# DEPRECATED as of 0.8.0, use noise_reduction instead
|
||||
; gravity = 100
|
||||
|
||||
|
||||
# In bar height, bars that would have been lower that this will not be drawn.
|
||||
# DEPRECATED as of 0.8.0
|
||||
; ignore = 0
|
||||
|
||||
# Noise reduction, int 0 - 100. default 77
|
||||
# the raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth
|
||||
# 100 will be very slow and smooth, 0 will be fast but noisy.
|
||||
; noise_reduction = 77
|
||||
|
||||
|
||||
[eq]
|
||||
|
||||
# This one is tricky. You can have as much keys as you want.
|
||||
# Remember to uncomment more than one key! More keys = more precision.
|
||||
# Look at readme.md on github for further explanations and examples.
|
||||
; 1 = 1 # bass
|
||||
; 2 = 1
|
||||
; 3 = 1 # midtone
|
||||
; 4 = 1
|
||||
; 5 = 1 # treble
|
||||
@@ -1,79 +0,0 @@
|
||||
#version 330
|
||||
|
||||
in vec2 fragCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
// bar values. defaults to left channels first (low to high), then right (high to low).
|
||||
uniform float bars[512];
|
||||
|
||||
uniform int bars_count; // number of bars (left + right) (configurable)
|
||||
uniform int bar_width; // bar width (configurable), not used here
|
||||
uniform int bar_spacing; // space bewteen bars (configurable)
|
||||
|
||||
uniform vec3 u_resolution; // window resolution
|
||||
|
||||
//colors, configurable in cava config file (r,g,b) (0.0 - 1.0)
|
||||
uniform vec3 bg_color; // background color
|
||||
uniform vec3 fg_color; // foreground color
|
||||
|
||||
uniform int gradient_count;
|
||||
uniform vec3 gradient_colors[8]; // gradient colors
|
||||
|
||||
vec3 normalize_C(float y,vec3 col_1, vec3 col_2, float y_min, float y_max)
|
||||
{
|
||||
//create color based on fraction of this color and next color
|
||||
float yr = (y - y_min) / (y_max - y_min);
|
||||
return col_1 * (1.0 - yr) + col_2 * yr;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
// find which bar to use based on where we are on the x axis
|
||||
float x = u_resolution.x * fragCoord.x;
|
||||
int bar = int(bars_count * fragCoord.x);
|
||||
|
||||
//calculate a bar size
|
||||
float bar_size = u_resolution.x / bars_count;
|
||||
|
||||
//the y coordinate and bar values are the same
|
||||
float y = bars[bar];
|
||||
|
||||
// make sure there is a thin line at bottom
|
||||
if (y * u_resolution.y < 1.0)
|
||||
{
|
||||
y = 1.0 / u_resolution.y;
|
||||
}
|
||||
|
||||
//draw the bar up to current height
|
||||
if (y > fragCoord.y)
|
||||
{
|
||||
//make some space between bars basen on settings
|
||||
if (x > (bar + 1) * (bar_size) - bar_spacing)
|
||||
{
|
||||
fragColor = vec4(bg_color,1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gradient_count == 0)
|
||||
{
|
||||
fragColor = vec4(fg_color,1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
//find which color in the configured gradient we are at
|
||||
int color = int((gradient_count - 1) * fragCoord.y);
|
||||
|
||||
//find where on y this and next color is supposed to be
|
||||
float y_min = color / (gradient_count - 1.0);
|
||||
float y_max = (color + 1.0) / (gradient_count - 1.0);
|
||||
|
||||
//make color
|
||||
fragColor = vec4(normalize_C(fragCoord.y, gradient_colors[color], gradient_colors[color + 1], y_min, y_max), 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fragColor = vec4(bg_color,1.0);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
#version 330
|
||||
|
||||
// this shader was stolen from shadertoy user ChunderFPV
|
||||
|
||||
#define SCALE 8.0
|
||||
#define PI radians(180.0)
|
||||
#define TAU (PI * 2.0)
|
||||
#define CS(a) vec2(cos(a), sin(a))
|
||||
#define PT(u, r) smoothstep(0.0, r, r - length(u))
|
||||
|
||||
in vec2 fragCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform float bars[512];
|
||||
|
||||
uniform int bars_count; // number of bars (left + right) (configurable)
|
||||
uniform float shader_time; // shader execution time s
|
||||
uniform int bar_width; // bar width (configurable), not used here
|
||||
uniform int bar_spacing; // space bewteen bars (configurable)
|
||||
|
||||
uniform vec3 u_resolution; // window resolution
|
||||
|
||||
// colors, configurable in cava config file (r,g,b) (0.0 - 1.0)
|
||||
uniform vec3 bg_color; // background color
|
||||
uniform vec3 fg_color; // foreground color
|
||||
|
||||
uniform int gradient_count;
|
||||
uniform vec3 gradient_colors[8]; // gradient colors
|
||||
|
||||
// gradient map ( color, equation, time, width, shadow, reciprocal )
|
||||
vec3 gm(vec3 c, float n, float t, float w, float d, bool i) {
|
||||
float g = min(abs(n), 1.0 / abs(n));
|
||||
float s = abs(sin(n * PI - t));
|
||||
if (i)
|
||||
s = min(s, abs(sin(PI / n + t)));
|
||||
return (1.0 - pow(abs(s), w)) * c * pow(g, d) * 6.0;
|
||||
}
|
||||
|
||||
// denominator spiral, use 1/n for numerator
|
||||
// ( screen xy, spiral exponent, decimal, line width, hardness, rotation )
|
||||
float ds(vec2 u, float e, float n, float w, float h, float ro) {
|
||||
float ur = length(u); // unit radius
|
||||
float sr = pow(ur, e); // spiral radius
|
||||
float a = round(sr) * n * TAU; // arc
|
||||
vec2 xy = CS(a + ro) * ur; // xy coords
|
||||
float l = PT(u - xy, w); // line
|
||||
float s = mod(sr + 0.5, 1.0); // gradient smooth
|
||||
s = min(s, 1.0 - s); // darken filter
|
||||
return l * s * h;
|
||||
}
|
||||
|
||||
void main() {
|
||||
float t = shader_time / PI * 2.0;
|
||||
vec4 m = vec4(0, 0, 0, 0); // iMouse;
|
||||
m.xy = m.xy * 2.0 / u_resolution.xy - 1.0; // ±1x, ±1y
|
||||
if (m.z > 0.0)
|
||||
t += m.y * SCALE; // move time with mouse y
|
||||
float z = (m.z > 0.0) ? pow(1.0 - abs(m.y), sign(m.y)) : 1.0; // zoom (+)
|
||||
float e = (m.z > 0.0) ? pow(1.0 - abs(m.x), -sign(m.x))
|
||||
: 1.0; // screen exponent (+)
|
||||
float se = (m.z > 0.0) ? e * -sign(m.y) : 1.0; // spiral exponent
|
||||
vec3 bg = vec3(0); // black background
|
||||
|
||||
float aa = 3.0; // anti-aliasing
|
||||
|
||||
for (float j = 0.0; j < aa; j++)
|
||||
for (float k = 0.0; k < aa; k++) {
|
||||
vec3 c = vec3(0);
|
||||
vec2 o = vec2(j, k) / aa;
|
||||
vec2 uv = (fragCoord * u_resolution.xy - 0.5 * u_resolution.xy + o) /
|
||||
u_resolution.y * SCALE * z; // apply cartesian, scale and zoom
|
||||
if (m.z > 0.0)
|
||||
uv =
|
||||
exp(log(abs(uv)) * e) * sign(uv); // warp screen space with exponent
|
||||
|
||||
float px = length(fwidth(uv)); // pixel width
|
||||
float x = uv.x; // every pixel on x
|
||||
float y = uv.y; // every pixel on y
|
||||
float l = length(uv); // hypot of xy: sqrt(x*x+y*y)
|
||||
|
||||
float mc = (x * x + y * y - 1.0) / y; // metallic circle at xy
|
||||
float g = min(abs(mc), 1.0 / abs(mc)); // gradient
|
||||
vec3 gold = vec3(1.0, 0.6, 0.0) * g * l;
|
||||
vec3 blue = vec3(0.3, 0.5, 0.9) * (1.0 - g);
|
||||
vec3 rgb = max(gold, blue);
|
||||
|
||||
float w = 0.1; // line width
|
||||
float d = 0.4; // shadow depth
|
||||
c = max(c, gm(rgb, mc, -t, w * bars[0], d, false)); // metallic
|
||||
c = max(c, gm(rgb, abs(y / x) * sign(y), -t, w * bars[1], d,
|
||||
false)); // tangent
|
||||
c = max(c, gm(rgb, (x * x) / (y * y) * sign(y), -t, w * bars[2], d,
|
||||
false)); // sqrt cotangent
|
||||
c = max(c, gm(rgb, (x * x) + (y * y), t, w * bars[3], d,
|
||||
true)); // sqrt circles
|
||||
|
||||
c += rgb * ds(uv, se, t / TAU, px * 2.0 * bars[4], 2.0, 0.0); // spiral 1a
|
||||
c += rgb * ds(uv, se, t / TAU, px * 2.0 * bars[5], 2.0, PI); // spiral 1b
|
||||
c +=
|
||||
rgb * ds(uv, -se, t / TAU, px * 2.0 * bars[6], 2.0, 0.0); // spiral 2a
|
||||
c += rgb * ds(uv, -se, t / TAU, px * 2.0 * bars[7], 2.0, PI); // spiral 2b
|
||||
c = max(c, 0.0); // clear negative color
|
||||
|
||||
c += pow(max(1.0 - l, 0.0), 3.0 / z); // center glow
|
||||
|
||||
if (m.z > 0.0) // display grid on click
|
||||
{
|
||||
vec2 xyg = abs(fract(uv + 0.5) - 0.5) / px; // xy grid
|
||||
c.gb += 0.2 * (1.0 - min(min(xyg.x, xyg.y), 1.0));
|
||||
}
|
||||
bg += c;
|
||||
}
|
||||
bg /= aa * aa;
|
||||
bg *= sqrt(bg) * 1.5;
|
||||
|
||||
fragColor = vec4(bg, 1.0);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#version 330
|
||||
|
||||
in vec2 fragCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
// bar values. defaults to left channels first (low to high), then right (high to low).
|
||||
uniform float bars[512];
|
||||
|
||||
uniform int bars_count; // number of bars (left + right) (configurable)
|
||||
|
||||
uniform vec3 u_resolution; // window resolution, not used here
|
||||
|
||||
//colors, configurable in cava config file
|
||||
uniform vec3 bg_color; // background color(r,g,b) (0.0 - 1.0), not used here
|
||||
uniform vec3 fg_color; // foreground color, not used here
|
||||
|
||||
void main()
|
||||
{
|
||||
// find which bar to use based on where we are on the x axis
|
||||
int bar = int(bars_count * fragCoord.x);
|
||||
|
||||
float bar_y = 1.0 - abs((fragCoord.y - 0.5)) * 2.0;
|
||||
float y = (bars[bar]) * bar_y;
|
||||
|
||||
float bar_x = (fragCoord.x - float(bar) / float(bars_count)) * bars_count;
|
||||
float bar_r = 1.0 - abs((bar_x - 0.5)) * 2;
|
||||
|
||||
bar_r = bar_r * bar_r * 2;
|
||||
|
||||
// set color
|
||||
fragColor.r = fg_color.x * y * bar_r;
|
||||
fragColor.g = fg_color.y * y * bar_r;
|
||||
fragColor.b = fg_color.z * y * bar_r;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#version 330
|
||||
|
||||
|
||||
// Input vertex data, different for all executions of this shader.
|
||||
layout(location = 0) in vec3 vertexPosition_modelspace;
|
||||
|
||||
// Output data ; will be interpolated for each fragment.
|
||||
out vec2 fragCoord;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(vertexPosition_modelspace,1);
|
||||
fragCoord = (vertexPosition_modelspace.xy+vec2(1,1))/2.0;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
#version 330
|
||||
|
||||
in vec2 fragCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
// bar values. defaults to left channels first (low to high), then right (high
|
||||
// to low).
|
||||
uniform float bars[512];
|
||||
|
||||
uniform int bars_count; // number of bars (left + right) (configurable)
|
||||
uniform int bar_width; // bar width (configurable), not used here
|
||||
uniform int bar_spacing; // space bewteen bars (configurable)
|
||||
|
||||
uniform vec3 u_resolution; // window resolution
|
||||
|
||||
// colors, configurable in cava config file (r,g,b) (0.0 - 1.0)
|
||||
uniform vec3 bg_color; // background color
|
||||
uniform vec3 fg_color; // foreground color
|
||||
|
||||
uniform int gradient_count;
|
||||
uniform vec3 gradient_colors[8]; // gradient colors
|
||||
|
||||
uniform sampler2D inputTexture; // Texture from the first render pass
|
||||
|
||||
vec3 normalize_C(float y, vec3 col_1, vec3 col_2, float y_min, float y_max) {
|
||||
// create color based on fraction of this color and next color
|
||||
float yr = (y - y_min) / (y_max - y_min);
|
||||
return col_1 * (1.0 - yr) + col_2 * yr;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// find which bar to use based on where we are on the y axis
|
||||
int bar = int(bars_count * fragCoord.y);
|
||||
float y = bars[bar];
|
||||
float band_size = 1.0 / float(bars_count);
|
||||
float current_band_min = bar * band_size;
|
||||
float current_band_max = (bar + 1) * band_size;
|
||||
|
||||
int hist_length = 512;
|
||||
float win_size = 1.0 / hist_length;
|
||||
|
||||
if (fragCoord.x > 1.0 - win_size) {
|
||||
|
||||
if (fragCoord.y > current_band_min && fragCoord.y < current_band_max) {
|
||||
|
||||
fragColor = vec4(fg_color * y, 1.0);
|
||||
}
|
||||
} else {
|
||||
vec2 offsetCoord = fragCoord;
|
||||
offsetCoord.x += float(win_size);
|
||||
fragColor = texture(inputTexture, offsetCoord);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
#version 330
|
||||
|
||||
// Emulate the "line style" spectrum analyzer from Winamp 2.
|
||||
// Try this config for a demonstration:
|
||||
|
||||
/*
|
||||
[general]
|
||||
bar_width = 2
|
||||
bar_spacing = 0
|
||||
higher_cutoff_freq = 22000
|
||||
|
||||
[output]
|
||||
method = sdl_glsl
|
||||
channels = mono
|
||||
fragment_shader = winamp_line_style_spectrum.frag
|
||||
|
||||
[color]
|
||||
background = '#000000'
|
||||
gradient = 1
|
||||
gradient_color_1 = '#319C08'
|
||||
gradient_color_2 = '#29CE10'
|
||||
gradient_color_3 = '#BDDE29'
|
||||
gradient_color_4 = '#DEA518'
|
||||
gradient_color_5 = '#D66600'
|
||||
gradient_color_6 = '#CE2910'
|
||||
|
||||
[smoothing]
|
||||
noise_reduction = 10
|
||||
*/
|
||||
|
||||
in vec2 fragCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
// bar values. defaults to left channels first (low to high), then right (high to low).
|
||||
uniform float bars[512];
|
||||
|
||||
uniform int bars_count; // number of bars (left + right) (configurable)
|
||||
uniform int bar_width; // bar width (configurable), not used here
|
||||
uniform int bar_spacing; // space bewteen bars (configurable)
|
||||
|
||||
uniform vec3 u_resolution; // window resolution
|
||||
|
||||
//colors, configurable in cava config file (r,g,b) (0.0 - 1.0)
|
||||
uniform vec3 bg_color; // background color
|
||||
uniform vec3 fg_color; // foreground color
|
||||
|
||||
uniform int gradient_count;
|
||||
uniform vec3 gradient_colors[8]; // gradient colors
|
||||
|
||||
vec3 normalize_C(float y,vec3 col_1, vec3 col_2, float y_min, float y_max)
|
||||
{
|
||||
//create color based on fraction of this color and next color
|
||||
float yr = (y - y_min) / (y_max - y_min);
|
||||
return col_1 * (1.0 - yr) + col_2 * yr;
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
// find which bar to use based on where we are on the x axis
|
||||
float x = u_resolution.x * fragCoord.x;
|
||||
int bar = int(bars_count * fragCoord.x);
|
||||
|
||||
//calculate a bar size
|
||||
float bar_size = u_resolution.x / bars_count;
|
||||
|
||||
//the y coordinate is stretched by 4X to resemble Winamp
|
||||
float y = min(bars[bar] * 4.0, 1.0);
|
||||
|
||||
// make sure there is a thin line at bottom
|
||||
if (y * u_resolution.y < 1.0)
|
||||
{
|
||||
y = 1.0 / u_resolution.y;
|
||||
}
|
||||
|
||||
vec4 bar_color;
|
||||
|
||||
if (gradient_count == 0)
|
||||
{
|
||||
bar_color = vec4(fg_color,1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
//find color in the configured gradient for the top of the bar
|
||||
int color = int((gradient_count - 1) * y);
|
||||
|
||||
//find where on y this and next color is supposed to be
|
||||
float y_min = float(color) / (gradient_count - 1.0);
|
||||
float y_max = float(color + 1) / (gradient_count - 1.0);
|
||||
|
||||
//make a solid color for the entire bar
|
||||
bar_color = vec4(normalize_C(y, gradient_colors[color], gradient_colors[color + 1], y_min, y_max), 1.0);
|
||||
}
|
||||
|
||||
|
||||
//draw the bar up to current height
|
||||
if (y > fragCoord.y)
|
||||
{
|
||||
//make some space between bars based on settings
|
||||
if (x > (bar + 1) * (bar_size) - bar_spacing)
|
||||
{
|
||||
fragColor = vec4(bg_color,1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
fragColor = bar_color;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fragColor = vec4(bg_color,1.0);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[color]
|
||||
background = '#001e26'
|
||||
foreground = '#708183'
|
||||
|
||||
gradient = 1
|
||||
gradient_color_1 = '#268bd2'
|
||||
gradient_color_2 = '#6c71c4'
|
||||
gradient_color_3 = '#cb4b16'
|
||||
|
||||
horizontal_gradient = 1
|
||||
horizontal_gradient_color_1 = '#586e75'
|
||||
horizontal_gradient_color_2 = '#b58900'
|
||||
horizontal_gradient_color_3 = '#839496'
|
||||
|
||||
blend_direction = 'up'
|
||||
@@ -1,10 +0,0 @@
|
||||
[color]
|
||||
horizontal_gradient = 1
|
||||
horizontal_gradient_color_1 = '#c45161'
|
||||
horizontal_gradient_color_2 = '#e094a0'
|
||||
horizontal_gradient_color_3 = '#f2b6c0'
|
||||
horizontal_gradient_color_4 = '#f2dde1'
|
||||
horizontal_gradient_color_5 = '#cbc7d8'
|
||||
horizontal_gradient_color_6 = '#8db7d2'
|
||||
horizontal_gradient_color_7 = '#5e62a9'
|
||||
horizontal_gradient_color_8 = '#434279'
|
||||
@@ -7,15 +7,20 @@ back into this repo.
|
||||
|
||||
## What's tracked here
|
||||
|
||||
- `settings.json` — global settings: permissions (deny git commit/push, `gh pr` write ops, and
|
||||
reads of secret files), default `model`, `enabledPlugins`, vim mode, dark theme, `effortLevel`,
|
||||
- `settings.json` — global settings: permissions (deny git push, `gh pr merge` / `close`,
|
||||
and reads of secret files; ask before `gh pr create` and `git rebase`; allow `gh` reads, `gh pr edit`, `gh workflow run`
|
||||
and `gh run rerun`; `git commit` is left to `hooks/git-guard.py`, which exempts listed repos), default `model`, `enabledPlugins`, vim mode, dark theme, `effortLevel`,
|
||||
fullscreen TUI, `statusLine`, `hooks`, `worktree` defaults, a generic `autoMode.environment`, …
|
||||
Nothing in here may name a specific project, path or remote: this file is published by the
|
||||
public mirror, and the auto-mode classifier only reads `autoMode` from user/managed settings
|
||||
anyway (never from a project's `.claude/settings*.json`).
|
||||
- `statusline.py` — rich status line (dir, git, model, context %, cost, disk — system metrics live in the tmux bar).
|
||||
- `statusline.py` — rich status line (dir, git, unresolved PR review threads, model, context %, cost, disk — system
|
||||
metrics live in the tmux bar).
|
||||
See [StatusBar.md](StatusBar.md) for what each segment and colour means.
|
||||
- `keybindings.json` — custom keybindings: vim-style scroll/navigation, plus `ctrl+x v` for the
|
||||
- `keybindings.json` — custom keybindings: vim-style scroll/navigation (`alt+j/k` and
|
||||
`ctrl+shift+j/k` scroll by line — kitty's own `kitty_mod+j/k` scroll only its main-screen
|
||||
scrollback and pass the key through to the fullscreen TUI's alternate screen, so they do not
|
||||
shadow these; dropped once by mistake on 2026-09-15), plus `ctrl+x v` for the
|
||||
verbose transcript view (`app:toggleTranscript`, same keystroke as opencode's `<leader>v`; the
|
||||
default `ctrl+o` still works, and `ctrl+e` inside the view expands everything).
|
||||
- `CLAUDE.md` — global user preferences applied to every project.
|
||||
@@ -29,17 +34,32 @@ back into this repo.
|
||||
installed, and always exits 0.
|
||||
- `hooks/git-guard.py` — PreToolUse hook on `Bash`: structurally blocks anything that commits,
|
||||
rewrites history or discards working-tree changes — `git commit` / `push` / `revert` /
|
||||
`cherry-pick` / `rebase` / `merge` (not `--abort`), `clean` / `reset` in any form that moves HEAD
|
||||
`cherry-pick` / `rebase -i|--autostash` / `merge` (not `--abort`), `clean` / `reset` in any form that moves HEAD
|
||||
or unstages everything (`reset -- <paths>`, `reset HEAD <path>` stay allowed) / `restore <paths>`
|
||||
(`--staged <path>` is fine, `--staged .` is not) / `checkout --` / `checkout .` / `stash drop|pop|clear` /
|
||||
`branch -D|-f|-M`, and `gh pr merge` / `create` / `close` / `edit` — wherever the flags sit
|
||||
`branch -D|-f|-M`, and `gh pr merge` / `close`, `gh pr create` without `--head` (gh would push
|
||||
the branch itself) and `gh pr edit` beyond `--title` / `--body` — wherever the flags sit
|
||||
(`git -C . push`, `/usr/bin/git push`, `sh -c 'git push'`), which the prefix-matched deny rules
|
||||
can't. The discard group is what protects parallel sessions sharing one checkout. Exit 2 + a
|
||||
can't. A plain `rebase` and its `--continue` / `--abort` pass: Claude rebases and fixes
|
||||
conflicts when asked (`CLAUDE.md`), and settings.json asks before each `git rebase`. The
|
||||
discard group is what protects parallel sessions sharing one checkout. Exit 2 + a
|
||||
one-line reason. Heredoc bodies are skipped on purpose (file contents). Regression matrix:
|
||||
`bash ~/.claude/hooks/git-guard-test.sh` (a file on purpose — typed inline, the commands under
|
||||
test would trip the live hook).
|
||||
test would trip the live hook). Repos listed in `~/.claude/git-guard-exempt` (host-local, not in
|
||||
this repo — it names projects; one main checkout per line, worktrees included): `commit` and
|
||||
`merge` pass there — judged from the hook input's
|
||||
`cwd`, every `cd` before the call and `git -C` — except `commit --amend` / `-a`. A user-scope deny
|
||||
cannot be lifted for one repo, so `git commit` is off the deny list: the hook is its only
|
||||
structural block.
|
||||
- `hooks/notify.py` — Notification hook (`idle_prompt|permission_prompt`): `notify-send` desktop
|
||||
notification when Claude is waiting on you; no-op on a headless host.
|
||||
- `../tmux/scripts/agent-state.sh` — UserPromptSubmit (busy) / Notification (input) / Stop (done) /
|
||||
StopFailure (error) / SessionEnd (off) hooks, all async: marks the tmux pane this session runs in
|
||||
(`@agent` pane option, drawn as ✓ ? ! after the window name by tmux.conf; visiting the pane
|
||||
greys it out, the next prompt removes it) and buzzes the phone for a new mark on a pane not in
|
||||
view (termux-notification, else a bell). Claude sessions run in daemon-spawned workers with no
|
||||
tmux env, so the script finds the pane by the session name Claude puts in the pane title. Shared with codex
|
||||
(`~/.codex/hooks.json`, untracked), pi and opencode. No-op outside tmux.
|
||||
- `hooks/drift-check.sh` — SessionStart + UserPromptSubmit hook, read-only: fails loudly when a
|
||||
`~/.config/claude/*` file has broken out of the repo into a plain file, or `~/.claude/*` no longer
|
||||
resolves into the dots repo. That is what Claude Code's atomic settings rewrite does through the
|
||||
@@ -55,14 +75,16 @@ back into this repo.
|
||||
- `skills/` — custom [Agent Skills](https://code.claude.com/docs/en/skills). See **Skills** below.
|
||||
- `link.sh` — idempotent bootstrap that creates the `~/.claude` symlinks below.
|
||||
- `../git/claude-commit-msg` + `../git/hooks/prepare-commit-msg` — the commit-message handoff.
|
||||
A session pipes its suggestion into `claude-commit-msg save`, which stores it as its own file under
|
||||
`$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/` **with the paths it staged**; on the next
|
||||
`git commit` / lazygit `C` the hook prefills the editor with the message(s) whose paths are part of
|
||||
that commit and consumes them (`used/`). Parallel sessions therefore never overwrite each other,
|
||||
and committing one session's files gets that session's message. `claude-commit-msg list` shows
|
||||
what is pending. The old single `CLAUDE_COMMIT_MSG` file is still honoured. Both tracked here
|
||||
(deployed to `~/.config/git/`); the hook is installed **per repo** — see setup step 4 — and
|
||||
deliberately not via a global `core.hooksPath`, which would override a project's own hooks.
|
||||
A session pipes its suggestion into `claude-commit-msg save` (run inside the target repo), which
|
||||
stores it as its own file under `$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/` **with the paths
|
||||
it staged**; on the next `git commit` / lazygit `C` the hook prefills the editor with the message(s)
|
||||
whose paths are part of that commit and consumes them (`used/`). Parallel sessions therefore never
|
||||
overwrite each other, and committing one session's files gets that session's message.
|
||||
`claude-commit-msg list` shows what is pending. The old single `CLAUDE_COMMIT_MSG` file is still
|
||||
honoured. Both tracked here (deployed to `~/.config/git/`); the hook is installed **per repo** — see
|
||||
setup step 4 — and deliberately not via a global `core.hooksPath`, which would override a project's
|
||||
own hooks. The prefill only runs for an editor commit: lazygit's `c` panel passes the message to git
|
||||
directly, so the hook skips and the suggestion stays pending.
|
||||
|
||||
The `settings.json` hook commands for the newer scripts are wrapped in `[ -f … ]`, so a host that
|
||||
has pulled the repo but not yet re-stowed just skips them instead of blocking every Bash call.
|
||||
@@ -93,7 +115,8 @@ claude
|
||||
```
|
||||
|
||||
```fish
|
||||
# 4. In every repo you commit in: install the one-shot commit-message prefill hook
|
||||
# 4. In every repo you commit in: install the commit-message prefill hook. New clones/inits get it
|
||||
# from ~/.config/git/template (init.templateDir) once that template hook is a symlink to it too.
|
||||
ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/prepare-commit-msg
|
||||
```
|
||||
|
||||
@@ -101,6 +124,12 @@ ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/p
|
||||
need to run `link.sh` by hand (`bash ~/.config/claude/link.sh`) if a link later gets clobbered
|
||||
(e.g. Claude's `/config` replaces one with a plain file) — it's idempotent and self-healing.
|
||||
|
||||
Tracked *files* in `~/.claude` link straight at the repo copy, not through `~/.config/claude`:
|
||||
Claude Code follows one symlink hop and then replaces the file atomically (temp + rename), so the
|
||||
old two-hop chain lost its middle link to a plain file on every settings write (`/model`, effort,
|
||||
plugins, `/config`) and the repo stopped seeing changes. With one hop the rename lands in the
|
||||
checkout. Dirs (`hooks`, `skills`) still go through `~/.config/claude`.
|
||||
|
||||
A **new skill/hook** added to the repo shows up after a re-stow — `./install.sh` (or `dotsync`) links
|
||||
the new file into `~/.config/claude`, which `link.sh` then surfaces into `~/.claude`.
|
||||
|
||||
@@ -122,8 +151,9 @@ so Claude never auto-triggers them. Run them inside the repo whose branch you're
|
||||
copy-paste markdown block, and copies it to the clipboard with `wl-copy`. Leaves verification
|
||||
checkboxes for you; never creates/pushes the PR.
|
||||
- **`/commit-msg [hint]`** — reads the **staged** diff, infers the repo's commit style from recent
|
||||
`git log` (e.g. this repo's `[Scope] summary`), drafts a matching message, prints it, and copies
|
||||
it to the clipboard with `wl-copy`. Never stages or commits — draft only.
|
||||
`git log` (e.g. this repo's `[Scope] summary`), drafts a matching message, prints it, and saves
|
||||
it for the `prepare-commit-msg` hook (lazygit `C` prefills it). Never stages or commits — draft
|
||||
only.
|
||||
- **`/verbose [on|off|status]`** — flips the *persisted* verbose view for the current project:
|
||||
`viewMode` (`verbose` / `default` — the key that actually decides the view and beats a sticky
|
||||
`/focus` choice for new sessions) plus `verbose` and `showThinkingSummaries`, written to the
|
||||
|
||||
@@ -8,13 +8,13 @@ resize), trailing (lowest-priority) segments are dropped from the right until it
|
||||
Wide — everything:
|
||||
|
||||
```
|
||||
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ 🔢 ↑63M ↓595k │ ♻️ 91% 1h │ 💾 81%
|
||||
📁 ~/.dots │ 🌿 .dots main* │ 💬 #42 3 · codex 2 │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ 🔢 ↑63M ↓595k │ ⚡ 63 t/s │ ♻️ 91% 1h │ 💾 81%
|
||||
```
|
||||
|
||||
Narrow — trailing segments trimmed:
|
||||
|
||||
```
|
||||
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h
|
||||
📁 ~/.dots │ 🌿 .dots main* │ 💬 #42 3 · codex 2 │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m
|
||||
```
|
||||
|
||||
Every segment is defensive: if its data is missing or a command fails, the segment is
|
||||
@@ -28,11 +28,13 @@ is supplementary to the main colored value.
|
||||
| | **Vim mode** | `[I]`/`[N]`/`[V]` editor mode | only with `editorMode: vim`; pair with `hideVimModeIndicator: true` so the built-in one doesn't duplicate it |
|
||||
| 📁 | **Directory** | Current working dir, with `~` for home | cyan |
|
||||
| 🌿 | **Git** | `repo branch` + `*` if dirty + `↑N`/`↓N` ahead/behind upstream | repo = magenta; branch = green when clean, yellow + red `*` when dirty; `↑` cyan, `↓` yellow. Omitted outside a repo |
|
||||
| 💬 | **PR review threads** | `#N` of the current branch's open PR + its **unresolved review threads** (any author, outdated ones included; `+` past 100), Codex's share broken out as `· codex N`; green `✓` when none are left | A thread counts for whoever opened it (its first comment's author; Codex = login matching `codex`, as in `/pr-loop`). PR-conversation comments are not counted — they have no resolved state, so a summary or an LGTM would never let it reach zero. A GitHub round trip is ~0.7 s, so a render only reads the per-branch cache `~/.claude/cache/statusline.pr.<hash>.json` and, once it is older than 90 s, spawns a detached `statusline.py --pr-refresh` that updates it (one at a time, via a `.lock` claim) — new counts show on the next render. A failed fetch (no GitHub remote — e.g. this repo on gitea —, gh not authed, offline) backs off 10 min and keeps the last counts with a gray `?`. Omitted with no `gh`, no open PR or a detached HEAD |
|
||||
| 🤖 | **Model** | Active model display name + `· effort` level (`low`/`medium`/`high`/`xhigh`) | name = blue, effort = light gray; effort omitted for models without the param |
|
||||
| 📝 | **Context** | `% of context window used` + `(Nk)` tokens | usage comes from the payload's `context_window` (`used_percentage` + `current_usage`, Claude Code 2.1.x); the transcript's last `message.usage` is only a fallback. Window from `context_window.context_window_size`, then `(1M …)` in the display name, else 200k — **not** from `model.id`, which arrives with any `[1m]` suffix already stripped. Adds a red **⚠compact** at ≥80% |
|
||||
| 📊 | **5h usage** | `% of the 5-hour rolling limit used` + time until it resets | from `rate_limits.five_hour`; Pro/Max only, and absent until the first API response of a session |
|
||||
| 💰 | **Cost** | `$` session cost so far + `$/h` burn rate | burn rate shown once the session exceeds ~30s |
|
||||
| 🔢 | **Session tokens** | `↑` input + `↓` output tokens used so far this session (cumulative, `k`/`M`) | summed over the transcript's assistant `message.usage` blocks, de-duplicated by message id (one line per content block shares the same usage). Input = `input + cache_creation + cache_read` — everything billed as input, so on a 1M session it dwarfs output (every call re-reads the cached prefix). Main transcript only: subagent usage lives in separate sidechain files. Incremental: a per-session state file `~/.claude/cache/statusline.tokens.<session>.json` remembers the byte offset already summed, so each refresh parses only the new tail (cold scan of a 6.5 MB transcript ≈ 70 ms, warm ≈ 0.1 ms); stale state files are pruned after 7 days once 50+ pile up. Omitted when there is no transcript; `↑` cyan, `↓` green, no thresholds |
|
||||
| 🔢 | **Session tokens** | `↑` input + `↓` output tokens used so far this session (cumulative, `k`/`M`) | summed over the transcript's assistant `message.usage` blocks, de-duplicated by message id (one line per content block shares the same usage). Input = `input + cache_creation + cache_read` — everything billed as input, so on a 1M session it dwarfs output (every call re-reads the cached prefix). Main transcript only: subagent usage lives in separate sidechain files. Incremental: a per-session state file `~/.claude/cache/statusline.tokens.<session>.json` remembers the byte offset already summed, so each refresh parses only the new tail (warm ≈ 0.2 ms), at most 8 MiB per refresh (≈ 150 ms) — Claude Code kills a status script when the next refresh fires, so a cold scan of a huge resumed transcript is spread over a few refreshes (totals climb, then settle) instead of restarting from zero each time. Stale state files are pruned after 7 days once 50+ pile up. Omitted when there is no transcript; `↑` cyan, `↓` green, no thresholds |
|
||||
| ⚡ | **Decode speed** | `N t/s` — tokens per second the model is generating | duration-weighted mean over the last 8 thinking blocks of the main conversation: `thinking_tokens / thinkingDurationMs` from the same transcript scan (blocks under 1 s or 50 tokens skipped). The only clean per-call timing Claude Code records: the payload's `cost.total_api_duration_ms` also absorbs subagent and advisor calls (it grew 28 s in a 12 s wall interval with no main-conversation completion) and its `output_tokens` is sometimes a mid-stream partial, so neither can give a rate. Includes first-token latency, so short blocks read a little low and long ones converge on the real decode speed (Fable 5.1 measured 60–85 t/s). Needs thinking enabled — omitted until a thinking block has completed, never shown with thinking off |
|
||||
| ♻️ | **Prompt cache** | hit `%` (higher is better) + cache TTL (`5m` / `1h`), yellow `·cold` once it has expired | from the payload's `prompt_cache` (2.1.260+: `hit_ratio` 0–1, `ttl`, `warm`); computed from usage on older payloads. A `1h` TTL is what keeps a 10-min `/pr-loop` cadence warm |
|
||||
| 💾 | **Disk** | `% used` of the filesystem at the cwd | from `statvfs` |
|
||||
|
||||
@@ -44,7 +46,9 @@ attention. Light gray = secondary detail. Thresholds (`green < … < yellow <
|
||||
| Segment | green | yellow | red |
|
||||
|---|---|---|---|
|
||||
| Context | `< 50%` | `50–80%` | `≥ 80%` (⚠compact) |
|
||||
| PR review threads | `✓` none unresolved | thread count | `codex N` |
|
||||
| 5h usage | `< 50%` | `50–80%` | `≥ 80%` |
|
||||
| Decode speed (inverted) | `≥ 40 t/s` | `20–40 t/s` | `< 20 t/s` |
|
||||
| Prompt cache (inverted) | `≥ 80%` | `50–80%` | `< 50%` |
|
||||
| Disk | `< 75%` | `75–90%` | `≥ 90%` |
|
||||
|
||||
@@ -74,5 +78,6 @@ events, so system metrics sat visibly stale between turns — the tmux status ba
|
||||
## Where it lives
|
||||
|
||||
`statusline.py` is tracked in this repo and symlinked to `~/.claude/statusline.py`;
|
||||
`settings.json` runs it via `statusLine` (`/usr/bin/env python3 ~/.claude/statusline.py`).
|
||||
Edits take effect on the next status refresh.
|
||||
`settings.json` runs it via `statusLine` (`/usr/bin/env python3 ~/.claude/statusline.py`) on every
|
||||
message event and, through `refreshInterval: 60`, once a minute while the session idles — so the PR
|
||||
count moves while you wait on a review. Edits take effect on the next status refresh.
|
||||
@@ -1,12 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression matrix for git-guard.py: `bash ~/.claude/hooks/git-guard-test.sh` (exit 1 on mismatch).
|
||||
# Kept as a file on purpose: typed inline, the very commands under test would trip the live hook.
|
||||
G="$(dirname "$(readlink -f "$0")")/git-guard.py"; fail=0
|
||||
G="$(dirname "$(readlink -f "$0")")/git-guard.py"
|
||||
fail=0
|
||||
# No exempt repository for the rows that do not ask for one, whatever this host's ~/.claude/git-guard-exempt says.
|
||||
export GIT_GUARD_EXEMPT=""
|
||||
# `git reset <one arg>` is allowed only for an existing path: give the matrix one to point at.
|
||||
tmp=$(mktemp -d) && cd "$tmp" && touch existing.py
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
run() { printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$2")}}" | python3 "$G" 2>/dev/null; rc=$?
|
||||
got=allow; [ $rc -eq 2 ] && got=block; [ "$got" = "$1" ] && mark=ok || { mark=MISMATCH; fail=1; }; printf '%-8s %-6s %-6s %s\n' "$mark" "$1" "$got" "$(printf '%s' "$2" | head -1)"; }
|
||||
run() {
|
||||
printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$2")}}" | python3 "$G" 2>/dev/null
|
||||
rc=$?
|
||||
got=allow
|
||||
[ $rc -eq 2 ] && got=block
|
||||
[ "$got" = "$1" ] && mark=ok || {
|
||||
mark=MISMATCH
|
||||
fail=1
|
||||
}
|
||||
printf '%-8s %-6s %-6s %s\n' "$mark" "$1" "$got" "$(printf '%s' "$2" | head -1)"
|
||||
}
|
||||
echo "--- must block: commits / history ---"
|
||||
run block 'git push'
|
||||
run block 'git -C /home/coja/.dots push origin main'
|
||||
@@ -28,7 +40,10 @@ run block 'git commit -m "fix: grep pattern; not a command"'
|
||||
run block 'git revert HEAD'
|
||||
run block 'git cherry-pick abc123'
|
||||
run block 'git rebase -i HEAD~3'
|
||||
run block 'git rebase main'
|
||||
run block 'git rebase --interactive main'
|
||||
run block 'git -C . rebase -i --autosquash main'
|
||||
run block 'git rebase --autostash origin/main'
|
||||
run block 'git -c rebase.autoStash=true rebase main'
|
||||
run block 'git merge feature'
|
||||
run block 'git merge --no-ff feature'
|
||||
echo "--- must block: discards working-tree changes ---"
|
||||
@@ -63,11 +78,16 @@ run block 'git stash clear'
|
||||
run block 'git branch -D feature'
|
||||
run block 'git branch -f main HEAD~1'
|
||||
run block 'git branch -M main trunk'
|
||||
echo "--- must block: gh pr write ops ---"
|
||||
echo "--- must block: gh pr write ops (create only with --head, edit only title/body) ---"
|
||||
run block 'gh pr merge 12 --squash'
|
||||
run block 'gh -R o/r pr create --fill'
|
||||
run block 'gh pr edit 3 --title x'
|
||||
run block 'gh pr close 4'
|
||||
run block 'gh -R o/r pr create --fill'
|
||||
run block 'gh pr create --title x --body y'
|
||||
run block 'gh pr create --dry-run --fill'
|
||||
run block 'gh pr edit 3 --add-reviewer bob'
|
||||
run block 'gh pr edit 3 --title x --base main'
|
||||
run block 'gh pr edit --add-label=bug 3'
|
||||
run block 'gh pr edit 3 -b y -m v2'
|
||||
echo "--- must allow ---"
|
||||
run allow 'git status'
|
||||
run allow 'git log -1 --oneline'
|
||||
@@ -88,6 +108,13 @@ run allow 'git checkout main'
|
||||
run allow 'git checkout -b feature'
|
||||
run allow 'git switch main'
|
||||
run allow 'git merge --abort'
|
||||
run allow 'git rebase main'
|
||||
run allow 'git rebase origin/main'
|
||||
run allow 'git rebase -Xtheirs main'
|
||||
run allow 'git rebase --continue'
|
||||
run allow 'git rebase --abort'
|
||||
run allow 'git -c core.editor=true rebase --continue'
|
||||
run allow 'GIT_EDITOR=true git rebase --continue'
|
||||
run allow 'git stash'
|
||||
run allow 'git stash list'
|
||||
run allow 'git stash push -m wip'
|
||||
@@ -110,12 +137,87 @@ run allow 'python3 - <<EOF
|
||||
print("git commit is manual")
|
||||
EOF'
|
||||
run allow 'gh pr view 12 --json updatedAt'
|
||||
run allow 'gh pr create --head feat --title x --body y'
|
||||
run allow 'gh pr create -H feat --fill'
|
||||
run allow 'gh -R o/r pr create --head=feat --fill'
|
||||
run allow 'gh pr create --help'
|
||||
run allow 'gh pr edit 3 --title x'
|
||||
run allow 'gh pr edit 3 -t x -b "- item --base"'
|
||||
run allow 'gh pr edit --body-file - 12'
|
||||
run allow 'gh pr edit 12 -F body.md --repo o/r'
|
||||
run allow 'gh pr edit 12 --title=x -bBody'
|
||||
run allow 'gh workflow run ci.yml --ref main'
|
||||
run allow 'gh run rerun 123 --failed'
|
||||
run allow 'gh api graphql -f query="mutation{...}"'
|
||||
run allow 'gh auth status'
|
||||
run allow 'ls -la HEAD#x; git status'
|
||||
run allow 'git log --grep="git push" -5'
|
||||
run allow '~/.config/git/claude-commit-msg list'
|
||||
echo "--- exempt repo (GIT_GUARD_EXEMPT = a scratch repo; commit and merge pass there, nothing else) ---"
|
||||
# runin <expect> <cwd> <command>: the hook input carries the session's cwd, as Claude Code sends it.
|
||||
R=$tmp/repo
|
||||
WT=$tmp/repo-wt
|
||||
git init -q "$R" && git -C "$R" -c user.name=t -c user.email=t@t commit -q --allow-empty -m init \
|
||||
&& git -C "$R" worktree add -q "$WT" 2>/dev/null && mkdir -p "$R/sub" && git init -q "$tmp/other"
|
||||
runin() {
|
||||
printf '%s' "$(python3 -c 'import json,sys; print(json.dumps({"tool_name":"Bash","tool_input":{"command":sys.argv[1]},"cwd":sys.argv[2]}))' "$3" "$2")" | GIT_GUARD_EXEMPT=$R python3 "$G" 2>/dev/null
|
||||
rc=$?
|
||||
got=allow
|
||||
[ $rc -eq 2 ] && got=block
|
||||
[ "$got" = "$1" ] && mark=ok || {
|
||||
mark=MISMATCH
|
||||
fail=1
|
||||
}
|
||||
printf '%-8s %-6s %-6s [%s] %s\n' "$mark" "$1" "$got" "${2#"$tmp"/}" "$(printf '%s' "$3" | head -1)"
|
||||
}
|
||||
runin allow "$R" 'git commit -m "docs: x"'
|
||||
runin allow "$R" 'git commit -m x -- docs/usage.md'
|
||||
runin allow "$R" 'git commit -m"fix a bug"'
|
||||
runin allow "$R" 'git commit -m "-a is not the flag here"'
|
||||
runin allow "$R" 'git commit --allow-empty -m x'
|
||||
runin allow "$R/sub" 'git commit -m x'
|
||||
runin allow "$R" 'git merge --ff-only feature'
|
||||
runin allow "$R" 'git merge --no-ff feature'
|
||||
runin allow "$WT" 'git commit -m x'
|
||||
runin allow "$tmp" "git -C $WT commit -m x"
|
||||
runin allow "$tmp" "cd $R && git commit -m x"
|
||||
runin allow "$tmp" 'cd repo && git commit -m x'
|
||||
runin allow "$tmp" "cd $WT; git status; git merge main"
|
||||
runin allow "$R" "sh -c 'git commit -m x'"
|
||||
runin block "$R" 'git push'
|
||||
runin block "$R" 'git commit --amend --no-edit'
|
||||
runin block "$R" 'git commit -am wip'
|
||||
runin block "$R" 'git commit -qa -m wip'
|
||||
runin block "$R" 'git commit --all -m wip'
|
||||
runin block "$R" 'git rebase -i main'
|
||||
runin block "$R" 'git cherry-pick abc123'
|
||||
runin block "$R" 'git revert HEAD'
|
||||
runin block "$R" 'git reset --hard HEAD~1'
|
||||
runin block "$R" 'git checkout -- file.py'
|
||||
runin block "$R" 'GIT_DIR=/x/.git git commit -m x'
|
||||
runin block "$R" 'env GIT_WORK_TREE=/x git commit -m x'
|
||||
runin block "$R" 'git --git-dir=/x/.git commit -m x'
|
||||
runin block "$R" 'git -C /tmp commit -m x'
|
||||
runin block "$R" 'git -C $(mktemp -d) commit -m x'
|
||||
runin block "$tmp" "cd $R && cd /tmp && git commit -m x"
|
||||
runin block "$R" 'cd - && git commit -m x'
|
||||
runin block "$R" 'cd $HOME && git commit -m x'
|
||||
runin block "$R" 'pushd /tmp && git commit -m x'
|
||||
runin block "$tmp" 'git commit -m x'
|
||||
runin block "$tmp/other" 'git commit -m x'
|
||||
runin block "$tmp" "git -C $tmp/other merge x"
|
||||
echo "--- fail-open ---"
|
||||
printf '%s' '{"tool_name":"Edit","tool_input":{"command":"git push"}}' | python3 "$G"; [ $? -eq 0 ] && echo "ok non-Bash tool ignored" || { echo "MISMATCH non-Bash"; fail=1; }
|
||||
printf 'not json' | python3 "$G"; [ $? -eq 0 ] && echo "ok garbage stdin passes" || { echo "MISMATCH garbage"; fail=1; }
|
||||
[ $fail -eq 0 ] && echo "GIT-GUARD MATRIX: all ok" || { echo "GIT-GUARD MATRIX: mismatches above"; exit 1; }
|
||||
printf '%s' '{"tool_name":"Edit","tool_input":{"command":"git push"}}' | python3 "$G"
|
||||
[ $? -eq 0 ] && echo "ok non-Bash tool ignored" || {
|
||||
echo "MISMATCH non-Bash"
|
||||
fail=1
|
||||
}
|
||||
printf 'not json' | python3 "$G"
|
||||
[ $? -eq 0 ] && echo "ok garbage stdin passes" || {
|
||||
echo "MISMATCH garbage"
|
||||
fail=1
|
||||
}
|
||||
[ $fail -eq 0 ] && echo "GIT-GUARD MATRIX: all ok" || {
|
||||
echo "GIT-GUARD MATRIX: mismatches above"
|
||||
exit 1
|
||||
}
|
||||
@@ -7,15 +7,24 @@ command text, so `git -C . push`, `git -c k=v commit`, `/usr/bin/git push`,
|
||||
This hook parses each shell segment instead: finds the git/gh token wherever it sits,
|
||||
skips git's global options (`-C dir`, `-c k=v`, `--git-dir=...`) and blocks
|
||||
|
||||
git commit | push | revert | cherry-pick | rebase | merge (not --abort/--quit/--continue)
|
||||
git commit | push | revert | cherry-pick | merge (not --abort/--quit/--continue)
|
||||
git rebase -i / --interactive / --autostash
|
||||
git clean | reset (any form that moves HEAD or unstages everything; `reset -- <paths>`,
|
||||
`reset HEAD <path>` and `reset <existing path>` are fine) | restore <paths> (--staged
|
||||
alone is fine, except `--staged .` / `:/`) | checkout -- / checkout . / -f
|
||||
git stash drop | pop | clear git branch -D / -f / -M
|
||||
gh pr merge | create | close | edit
|
||||
gh pr merge | close | create without --head | edit beyond --title/--body
|
||||
|
||||
i.e. anything that commits, rewrites history, or discards working-tree changes -- the last
|
||||
group matters most with several sessions sharing one checkout. Mirrors pi's permission-gate.
|
||||
On GitHub Claude may open a PR (settings.json asks first) and retitle / redescribe one, nothing
|
||||
more: `gh pr create` must name its branch with --head, without which gh pushes it itself (even
|
||||
under --dry-run), and `gh pr edit` must not touch reviewers, labels, base or the like.
|
||||
A plain `git rebase` (and its --continue / --abort) passes: Claude rebases and fixes the
|
||||
conflicts when the user asks (global CLAUDE.md), and settings.json asks before each one, as
|
||||
pi's gate does. `-i` stays blocked (the squash/reword tool, and it needs an editor), and so
|
||||
does --autostash: it stashes and re-pops other sessions' uncommitted edits, where a plain
|
||||
rebase refuses to start on a dirty tree.
|
||||
|
||||
Exit 2 + one stderr line = the call is blocked and Claude reads the reason (stage and
|
||||
stop; /commit-msg drafts the message). Mirrors pi's permission-gate.ts.
|
||||
@@ -31,15 +40,27 @@ README that documents pushing must stay writable -- so a script piped in through
|
||||
is not inspected. Aliases (`git ci`) are not expanded. The deny list, the auto-mode
|
||||
classifier and the global CLAUDE.md still cover those; this is the belt for the prefix
|
||||
rules' braces. Fails open: any parse error lets the command through (exit 0).
|
||||
|
||||
Exempt repositories (one main checkout per line in ~/.claude/git-guard-exempt, host-local and
|
||||
outside the dots repo on purpose: it names projects): there `git commit` and `git merge` pass
|
||||
-- the user's decision for repos where Claude commits and merges itself. The repo is the one the call
|
||||
actually targets: the hook input's `cwd`, moved by every `cd` before it in the command and by
|
||||
`git -C`, resolved to its main checkout (so its worktrees count). Anything that hides the
|
||||
target (`cd -`, `cd $VAR`, a substitution, `--git-dir`, `GIT_DIR=`) is judged not exempt.
|
||||
`commit --amend` and `commit -a`/`--all` stay blocked there (they rewrite the last commit of
|
||||
a shared checkout, or sweep other sessions' edits in), as do push, `rebase -i` and the discard
|
||||
group. Known gap: a `cd` inside `( ... )` leaks into the commands after the parenthesis.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
GIT_BLOCK = {"commit", "push", "clean", "revert", "cherry-pick", "rebase"}
|
||||
GIT_BLOCK = {"commit", "push", "clean", "revert", "cherry-pick"}
|
||||
MERGE_OK = {"--abort", "--quit", "--continue"}
|
||||
REBASE_BLOCK = {"-i", "--interactive", "--autostash"}
|
||||
STASH_BLOCK = {"drop", "pop", "clear"}
|
||||
BRANCH_FORCE = {"-D", "-f", "--force", "-M"}
|
||||
RESET_MODES = {"--hard", "--soft", "--merge", "--keep"}
|
||||
@@ -50,7 +71,20 @@ _HEX = re.compile(r"^[0-9a-f]{7,40}$")
|
||||
def looks_like_ref(a):
|
||||
return (a in ("HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD") or a.startswith("refs/")
|
||||
or "~" in a or "^" in a or "@{" in a or bool(_HEX.match(a)))
|
||||
GH_PR_BLOCK = {"merge", "create", "close", "edit"}
|
||||
GH_PR_BLOCK = {"merge", "close"}
|
||||
# `gh pr edit` may only retitle and redescribe; these take their value as the next word (or as
|
||||
# `--title=x` / `-tx`).
|
||||
GH_EDIT_OK = {"-t", "--title", "-b", "--body", "-F", "--body-file", "-R", "--repo"}
|
||||
# Repositories (main checkouts; their worktrees count) where Claude commits and merges itself:
|
||||
# one path per line, `#` comments; a missing file exempts nothing. GIT_GUARD_EXEMPT
|
||||
# (colon-separated) replaces the file for the test matrix; it is read from the hook's own
|
||||
# environment (Claude Code's), which a prefix on the checked command cannot reach.
|
||||
EXEMPT_FILE = "~/.claude/git-guard-exempt"
|
||||
EXEMPT_VERBS = {"commit", "merge"}
|
||||
# `git commit` options whose value is the next word (or, for the short ones, the rest of the word).
|
||||
COMMIT_ARG_SHORT = set("mFCct")
|
||||
COMMIT_ARG_LONG = {"--message", "--file", "--author", "--date", "--reuse-message", "--reedit-message",
|
||||
"--fixup", "--squash", "--template", "--trailer", "--cleanup", "--pathspec-from-file"}
|
||||
# global options that take a *separate* argument; the `--opt=value` spelling is one token.
|
||||
GIT_OPT_ARG = {"-C", "-c", "--git-dir", "--work-tree", "--exec-path", "--namespace",
|
||||
"--super-prefix", "--config-env"}
|
||||
@@ -72,13 +106,102 @@ def _skip_opts(args, with_arg):
|
||||
return j
|
||||
|
||||
|
||||
def git_reason(args):
|
||||
def exempt_repos():
|
||||
env = os.environ.get("GIT_GUARD_EXEMPT")
|
||||
if env is not None:
|
||||
paths = env.split(":")
|
||||
else:
|
||||
try:
|
||||
with open(os.path.expanduser(EXEMPT_FILE)) as f:
|
||||
paths = [ln.split("#", 1)[0].strip() for ln in f]
|
||||
except OSError:
|
||||
return set()
|
||||
return {os.path.realpath(os.path.expanduser(p)) for p in paths if p}
|
||||
|
||||
|
||||
def main_checkout(d):
|
||||
"""The main checkout of the repository `d` lies in (a worktree resolves to it), or None."""
|
||||
try:
|
||||
out = subprocess.run(["git", "-C", d, "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
||||
capture_output=True, text=True, timeout=3)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
common = out.stdout.strip().rstrip("/")
|
||||
if out.returncode != 0 or not common:
|
||||
return None
|
||||
return os.path.realpath(os.path.dirname(common))
|
||||
|
||||
|
||||
def in_exempt_repo(opts, cwd, pre):
|
||||
"""Whether a git call with global options `opts`, run from `cwd` after the words `pre` of
|
||||
its own command (`env X=1`, ...), targets an exempt repository."""
|
||||
exempt = exempt_repos()
|
||||
if not exempt or cwd is None or any(t.startswith(("GIT_DIR=", "GIT_WORK_TREE=", "GIT_COMMON_DIR="))
|
||||
for t in pre):
|
||||
return False
|
||||
d, j = cwd, 0
|
||||
while j < len(opts):
|
||||
a = opts[j]
|
||||
if a in ("--git-dir", "--work-tree") or a.startswith(("--git-dir=", "--work-tree=")):
|
||||
return False
|
||||
if a == "-C":
|
||||
if j + 1 >= len(opts) or "SUBST" in opts[j + 1] or "$" in opts[j + 1]:
|
||||
return False
|
||||
d = os.path.join(d, os.path.expanduser(opts[j + 1]))
|
||||
j += 2 if a in GIT_OPT_ARG else 1
|
||||
return main_checkout(d) in exempt
|
||||
|
||||
|
||||
def commit_rewrites_or_sweeps(rest):
|
||||
"""`commit --amend` (rewrites the last commit) or `-a`/`--all` (takes in every modified
|
||||
tracked file, other sessions' edits included)."""
|
||||
skip = False
|
||||
for a in rest:
|
||||
if skip:
|
||||
skip = False
|
||||
continue
|
||||
if a == "--":
|
||||
break
|
||||
if a in ("--amend", "--all"):
|
||||
return True
|
||||
if a in COMMIT_ARG_LONG:
|
||||
skip = True
|
||||
elif a.startswith("-") and not a.startswith("--"):
|
||||
for k, ch in enumerate(a[1:], 1):
|
||||
if ch == "a":
|
||||
return True
|
||||
if ch in COMMIT_ARG_SHORT:
|
||||
skip = k == len(a) - 1 # the value is the next word
|
||||
break
|
||||
return False
|
||||
|
||||
|
||||
def change_dir(cwd, args):
|
||||
"""Where `cd args` leaves the shell started in `cwd`; None when that cannot be told."""
|
||||
args = [a for a in args if a not in ("-L", "-P", "--")]
|
||||
if not args:
|
||||
return os.path.expanduser("~")
|
||||
t = args[0]
|
||||
if t == "-" or "SUBST" in t or "$" in t or (cwd is None and not t.startswith(("/", "~"))):
|
||||
return None
|
||||
return os.path.normpath(os.path.join(cwd or "/", os.path.expanduser(t)))
|
||||
|
||||
|
||||
def git_reason(args, cwd=None, pre=()):
|
||||
j = _skip_opts(args, GIT_OPT_ARG)
|
||||
if j >= len(args):
|
||||
return None
|
||||
sub, rest = args[j], args[j + 1:]
|
||||
if (sub in EXEMPT_VERBS and not (sub == "commit" and commit_rewrites_or_sweeps(rest))
|
||||
and in_exempt_repo(args[:j], cwd, pre)):
|
||||
return None
|
||||
if sub in GIT_BLOCK:
|
||||
return f"git {sub}"
|
||||
if sub == "rebase":
|
||||
flag = next((a for a in rest if a in REBASE_BLOCK), None)
|
||||
if flag == "--autostash" or any(o.lower().startswith("rebase.autostash") for o in args[:j]):
|
||||
return "git rebase --autostash (stashes other sessions' uncommitted edits)"
|
||||
return f"git rebase {flag} (interactive history rewrite)" if flag else None
|
||||
if sub == "reset":
|
||||
opts = [a for a in rest if a.startswith("-") and a != "--"]
|
||||
if RESET_MODES & set(opts):
|
||||
@@ -109,14 +232,41 @@ def git_reason(args):
|
||||
return None
|
||||
|
||||
|
||||
def gh_edit_extra(rest):
|
||||
"""The first `gh pr edit` option that is neither the title nor the body, else None."""
|
||||
k = 0
|
||||
while k < len(rest):
|
||||
a = rest[k]
|
||||
if a == "--":
|
||||
break
|
||||
name = a.split("=", 1)[0]
|
||||
if name in GH_EDIT_OK:
|
||||
k += 2 if name == a else 1 # skip the value, which may well start with `-`
|
||||
continue
|
||||
if a.startswith("-") and not (a[:2] in GH_EDIT_OK and not a.startswith("--")):
|
||||
return a
|
||||
k += 1
|
||||
return None
|
||||
|
||||
|
||||
def gh_reason(args):
|
||||
j = _skip_opts(args, GH_OPT_ARG)
|
||||
if j >= len(args) or args[j] != "pr":
|
||||
return None
|
||||
rest = args[j + 1:]
|
||||
k = _skip_opts(rest, GH_OPT_ARG)
|
||||
if k < len(rest) and rest[k] in GH_PR_BLOCK:
|
||||
return f"gh pr {rest[k]}"
|
||||
if k >= len(rest) or "--help" in rest:
|
||||
return None
|
||||
sub, rest = rest[k], rest[k + 1:]
|
||||
if sub in GH_PR_BLOCK:
|
||||
return f"gh pr {sub}"
|
||||
if sub == "create" and not any(a in ("-H", "--head") or a.startswith("--head=")
|
||||
or (a.startswith("-H") and not a.startswith("--")) for a in rest):
|
||||
return "gh pr create without --head: gh would push the branch itself"
|
||||
if sub == "edit":
|
||||
extra = gh_edit_extra(rest)
|
||||
if extra:
|
||||
return f"gh pr edit {extra.split('=', 1)[0]}: only --title / --body edits are allowed"
|
||||
return None
|
||||
|
||||
|
||||
@@ -146,8 +296,8 @@ def segments(toks):
|
||||
yield seg
|
||||
|
||||
|
||||
def scan(text, depth=0):
|
||||
"""Reason string for the first blocked invocation in `text`, else None."""
|
||||
def scan(text, depth=0, cwd=None):
|
||||
"""Reason string for the first blocked invocation in `text` (run from `cwd`), else None."""
|
||||
if depth > 4:
|
||||
return None
|
||||
text = _HEREDOC.sub(" ", text)
|
||||
@@ -163,23 +313,29 @@ def scan(text, depth=0):
|
||||
while prev != text:
|
||||
prev, text = text, _SUBST.sub(_grab, text)
|
||||
for body in bodies:
|
||||
r = scan(body, depth + 1)
|
||||
r = scan(body, depth + 1, cwd)
|
||||
if r:
|
||||
return r
|
||||
for toks in segments(tokenize(text)):
|
||||
if toks[0] == "cd":
|
||||
cwd = change_dir(cwd, toks[1:])
|
||||
continue
|
||||
if toks[0] in ("pushd", "popd"):
|
||||
cwd = None
|
||||
continue
|
||||
for i, tok in enumerate(toks):
|
||||
base = os.path.basename(tok)
|
||||
r = None
|
||||
if base == "git":
|
||||
r = git_reason(toks[i + 1:])
|
||||
r = git_reason(toks[i + 1:], cwd, toks[:i])
|
||||
elif base == "gh":
|
||||
r = gh_reason(toks[i + 1:])
|
||||
elif base == "eval":
|
||||
r = scan(" ".join(toks[i + 1:]), depth + 1)
|
||||
r = scan(" ".join(toks[i + 1:]), depth + 1, cwd)
|
||||
elif base in SHELLS and "-c" in toks[i + 1:]:
|
||||
k = toks.index("-c", i + 1) # `sh -c '<cmd>'`: the real command is one word
|
||||
if k + 1 < len(toks):
|
||||
r = scan(toks[k + 1], depth + 1)
|
||||
r = scan(toks[k + 1], depth + 1, cwd)
|
||||
if r:
|
||||
return r
|
||||
return None
|
||||
@@ -191,16 +347,21 @@ def main():
|
||||
if data.get("tool_name", "Bash") != "Bash":
|
||||
return
|
||||
cmd = (data.get("tool_input") or {}).get("command") or ""
|
||||
reason = scan(cmd) if cmd else None
|
||||
reason = scan(cmd, 0, data.get("cwd") or os.getcwd()) if cmd else None
|
||||
except Exception:
|
||||
return # fail open: the deny list still applies
|
||||
if reason:
|
||||
short = cmd.strip().splitlines()[0]
|
||||
short = short if len(short) <= 80 else short[:77] + "..."
|
||||
sys.stderr.write(
|
||||
f"git-guard: blocked `{short}` ({reason}). The user commits, pushes and merges "
|
||||
"manually (global CLAUDE.md): stage and stop, and draft the message with /commit-msg.\n"
|
||||
)
|
||||
hint = ("On a PR Claude reads, opens one when asked (with --head <pushed branch>) and edits its "
|
||||
"--title / --body; pushing, merging, closing and every other edit are the user's."
|
||||
if reason.startswith("gh ") else
|
||||
"Rebase with a plain `git rebase` on a clean tree, and only when the user asked; if the "
|
||||
"tree holds other sessions' edits, tell the user instead of stashing them."
|
||||
if reason.startswith("git rebase") else
|
||||
"The user commits, pushes and merges manually (global CLAUDE.md): stage and stop, "
|
||||
"and draft the message with /commit-msg.")
|
||||
sys.stderr.write(f"git-guard: blocked `{short}` ({reason}). {hint}\n")
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
"context": "Global",
|
||||
"bindings": {
|
||||
"alt+j": "scroll:lineDown",
|
||||
"alt+k": "scroll:lineUp"
|
||||
"alt+k": "scroll:lineUp",
|
||||
"ctrl+shift+j": "scroll:lineDown",
|
||||
"ctrl+shift+k": "scroll:lineUp"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -41,6 +41,16 @@ items="settings.json statusline.py keybindings.json CLAUDE.md hooks skills"
|
||||
# Expose each item in ~/.claude (where Claude actually reads).
|
||||
for item in $items; do
|
||||
[ -e "$SRC/$item" ] || continue
|
||||
# Files link straight at the repo copy, not through ~/.config/claude: Claude Code
|
||||
# resolves ONE symlink hop, then writes atomically (temp + rename) at that path —
|
||||
# verified on 2.1.296 with `claude plugin disable` against a scratch CLAUDE_CONFIG_DIR.
|
||||
# Through a ~/.claude -> ~/.config/claude -> repo chain, every settings write (/model,
|
||||
# effort, plugins, /config) swapped the middle link for a plain file and severed the
|
||||
# repo. One hop puts the rename inside the checkout, so both links survive. A SRC file
|
||||
# that already drifted into a plain file keeps being the target (it may hold the newest
|
||||
# edits) until the drift guard below is reconciled. Dirs keep the stow path.
|
||||
target="$SRC/$item"
|
||||
[ -f "$repo/$item" ] && [ -L "$SRC/$item" ] && target="$repo/$item"
|
||||
if [ -d "$DST/$item" ] && [ ! -L "$DST/$item" ]; then
|
||||
echo "skip ~/.claude/$item (real directory in the way — move it aside first)" >&2
|
||||
continue
|
||||
@@ -62,14 +72,14 @@ for item in $items; do
|
||||
if [ "$same" != 1 ]; then
|
||||
echo "DRIFT ~/.claude/$item is a real file that differs from the repo copy —" >&2
|
||||
echo " it may hold newer edits. Review, then re-link:" >&2
|
||||
echo " diff '$DST/$item' '$SRC/$item' # compare; keep whichever is right" >&2
|
||||
echo " ln -sfn '$SRC/$item' '$DST/$item' # re-link" >&2
|
||||
echo " diff '$DST/$item' '$target' # compare; keep whichever is right" >&2
|
||||
echo " ln -sfn '$target' '$DST/$item' # re-link" >&2
|
||||
continue
|
||||
fi
|
||||
echo "healed ~/.claude/$item (plain copy identical to repo — re-linking)"
|
||||
fi
|
||||
ln -sfn "$SRC/$item" "$DST/$item"
|
||||
echo "linked ~/.claude/$item -> ~/.config/claude/$item"
|
||||
ln -sfn "$target" "$DST/$item"
|
||||
echo "linked ~/.claude/$item -> ${target/#$HOME/\~}"
|
||||
done
|
||||
|
||||
# Stow-layer drift guard: each SRC *file* should be a symlink into the repo, so edits
|
||||
|
||||
@@ -4,23 +4,60 @@
|
||||
"pr": ""
|
||||
},
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cat:*)",
|
||||
"Bash(head:*)",
|
||||
"Bash(tail:*)",
|
||||
"Bash(sed -n:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git show:*)",
|
||||
"Bash(git rev-parse:*)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git stash list:*)",
|
||||
"Bash(pnpm run:*)",
|
||||
"Bash(pnpm exec:*)",
|
||||
"Bash(node -p:*)",
|
||||
"Bash(gh pr view:*)",
|
||||
"Bash(gh pr list:*)",
|
||||
"Bash(gh pr diff:*)",
|
||||
"Bash(gh pr checks:*)",
|
||||
"Bash(gh pr status:*)",
|
||||
"Bash(gh issue view:*)",
|
||||
"Bash(gh issue list:*)",
|
||||
"Bash(gh issue status:*)",
|
||||
"Bash(gh run view:*)",
|
||||
"Bash(gh run list:*)",
|
||||
"Bash(gh run watch:*)",
|
||||
"Bash(gh workflow view:*)",
|
||||
"Bash(gh workflow list:*)",
|
||||
"Bash(gh repo view:*)",
|
||||
"Bash(gh release view:*)",
|
||||
"Bash(gh release list:*)",
|
||||
"Bash(gh search:*)",
|
||||
"Bash(gh label list:*)",
|
||||
"Bash(gh status:*)",
|
||||
"Bash(gh auth status:*)",
|
||||
"Bash(gh pr edit:*)",
|
||||
"Bash(gh workflow run:*)",
|
||||
"Bash(gh run rerun:*)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(git push)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git commit)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git reset --hard)",
|
||||
"Bash(git reset --hard:*)",
|
||||
"Bash(git clean)",
|
||||
"Bash(git clean:*)",
|
||||
"Bash(gh pr merge)",
|
||||
"Bash(gh pr merge:*)",
|
||||
"Bash(gh pr create)",
|
||||
"Bash(gh pr create:*)",
|
||||
"Bash(gh pr close)",
|
||||
"Bash(gh pr close:*)",
|
||||
"Bash(gh pr edit)",
|
||||
"Bash(gh pr edit:*)",
|
||||
"Read(**/.env)",
|
||||
"Read(**/.env.*)",
|
||||
"Read(**/*.env)",
|
||||
@@ -42,9 +79,15 @@
|
||||
"Read(**/.gnupg/**)",
|
||||
"Read(**/fish_variables)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(gh pr create)",
|
||||
"Bash(gh pr create:*)",
|
||||
"Bash(git rebase)",
|
||||
"Bash(git rebase:*)"
|
||||
],
|
||||
"defaultMode": "auto"
|
||||
},
|
||||
"model": "claude-fable-5-1[1m]",
|
||||
"model": "opus",
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
@@ -67,6 +110,16 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc busy; exit 0",
|
||||
"timeout": 5,
|
||||
"async": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptExpansion": [
|
||||
@@ -125,6 +178,51 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "idle_prompt|permission_prompt",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc input; exit 0",
|
||||
"timeout": 5,
|
||||
"async": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc done; exit 0",
|
||||
"timeout": 5,
|
||||
"async": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"StopFailure": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc error; exit 0",
|
||||
"timeout": 5,
|
||||
"async": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc off; exit 0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -136,13 +234,23 @@
|
||||
"type": "command",
|
||||
"command": "/usr/bin/env python3 ~/.claude/statusline.py",
|
||||
"padding": 0,
|
||||
"refreshInterval": 60,
|
||||
"hideVimModeIndicator": true
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"frontend-design@claude-plugins-official": true
|
||||
"frontend-design@claude-plugins-official": true,
|
||||
"rust-analyzer-lsp@claude-plugins-official": true
|
||||
},
|
||||
"effortLevel": "xhigh",
|
||||
"modelSettings": {
|
||||
"claude-fable-5-1": {
|
||||
"effortLevel": "xhigh"
|
||||
},
|
||||
"claude-opus-5-5": {
|
||||
"effortLevel": "xhigh"
|
||||
}
|
||||
},
|
||||
"advisorModel": "fable",
|
||||
"tui": "fullscreen",
|
||||
"skipWorkflowUsageWarning": true,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
description: Draft a commit message for the staged changes, matching this repo's commit style, then print it in the reply, save it to the repo's CLAUDE_COMMIT_MSG file, and copy it to the clipboard. Never stages or commits. Use once you've staged what you want to commit.
|
||||
description: Draft a commit message for the staged changes, matching this repo's commit style, then print it in the reply and save it to the repo's CLAUDE_COMMIT_MSG file. Never stages or commits. Use once you've staged what you want to commit.
|
||||
argument-hint: [optional focus/scope hint]
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(wl-copy:*), Bash(~/.config/git/claude-commit-msg:*), Read, Glob
|
||||
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(~/.config/git/claude-commit-msg:*), Read, Glob
|
||||
---
|
||||
|
||||
# Draft a commit message for the staged changes
|
||||
@@ -26,9 +26,8 @@ Context for the current commit — what's staged, plus this repo's recent style:
|
||||
- Add a short body (wrapped ~72 cols) only when the change needs the *why*; otherwise
|
||||
subject-only. Don't invent motivation that isn't evident from the diff.
|
||||
4. **Print the message in your reply first** — as a single fenced ` ```text ` block, copy-paste
|
||||
ready. This in-reply copy is the durable one: it lives in the transcript, so it survives even if
|
||||
the clipboard gets overwritten later (by me copying something else, or another Claude session
|
||||
running `wl-copy`). Always print it; don't rely on the clipboard alone.
|
||||
ready. This in-reply copy is the durable one: it lives in the transcript, so it survives even
|
||||
after the hook has consumed the handoff. Always print it.
|
||||
5. **Hand it off** so I can recover it any time — feed the raw message to the CLI as a quoted
|
||||
heredoc (so quotes, backticks and `$` in the body survive untouched):
|
||||
```sh
|
||||
@@ -41,8 +40,6 @@ Context for the current commit — what's staged, plus this repo's recent style:
|
||||
`prepare-commit-msg` hook prefills the next `git commit` / lazygit `C` with the message(s) whose
|
||||
paths are part of that commit, then consumes them. Never write `CLAUDE_COMMIT_MSG` or
|
||||
`COMMIT_EDITMSG` directly. The `save` output names the saved id and file path.
|
||||
6. **Also copy it to the clipboard** as a convenience: `wl-copy < <file path printed by save>`.
|
||||
Then tell me all three places it is: **printed above**, **saved for the hook** (with its id),
|
||||
and **on the clipboard** — so I never lose it to a stray copy.
|
||||
7. **Never** run `git add`, `git commit`, or `git push`, and never offer to — I stage and commit
|
||||
Then tell me both places it is: **printed above** and **saved for the hook** (with its id).
|
||||
6. **Never** run `git add`, `git commit`, or `git push`, and never offer to — I stage and commit
|
||||
manually. Draft only.
|
||||
@@ -2,7 +2,7 @@
|
||||
description: Babysit the current branch's PR — address review comments that don't need my input, re-check every ~10 min, and stop when there are no open comments left or the automated reviewers (Codex, Claude) hit their limit. Prep-only — stages fixes and drafts the commit message; never commits, pushes, or merges.
|
||||
argument-hint: [pr number or url — optional; defaults to the current branch's PR]
|
||||
disable-model-invocation: true
|
||||
allowed-tools: Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(wl-copy:*), Bash(~/.config/git/claude-commit-msg:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup
|
||||
allowed-tools: Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(~/.config/git/claude-commit-msg:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup
|
||||
---
|
||||
|
||||
# Watch this PR and resolve review comments (prep-only)
|
||||
@@ -238,8 +238,7 @@ Current state (auth + the current branch's PR):
|
||||
quoted heredoc, so quotes and backticks survive) — it is stored with the paths you staged this cycle, so the `prepare-commit-msg` hook prefills it only
|
||||
for the commit that contains them (other sessions' handoffs are not overwritten). Never write
|
||||
`CLAUDE_COMMIT_MSG` / `COMMIT_EDITMSG` directly.
|
||||
- **(c) Copy it:** `wl-copy < <file path printed by save>`.
|
||||
- Tell me all three locations. **Never** run `git commit`.
|
||||
- Tell me both locations. **Never** run `git commit`.
|
||||
(Skip this step when nothing new was staged this cycle.)
|
||||
|
||||
10. **Ping + schedule.** Give me a tight summary — deltas only, don't restate unchanged threads:
|
||||
@@ -271,9 +270,10 @@ Current state (auth + the current branch's PR):
|
||||
|
||||
- **Never** `git commit`, `git push`, `gh pr merge`, `gh pr create`, `gh pr close`, or `gh pr edit`
|
||||
— I do all of those. You stage, react, resolve, and draft; nothing more. The only comment you may
|
||||
post is a needs-input decision question. This is enforced, not just asked: `settings.json` denies
|
||||
those commands and the `git-guard.py` PreToolUse hook blocks any spelling of them, so a slip is
|
||||
refused with a reason rather than executed.
|
||||
post is a needs-input decision question. Commit, push, merge and close are enforced, not just
|
||||
asked: `settings.json` denies them and the `git-guard.py` PreToolUse hook blocks any spelling of
|
||||
them. `gh pr create` / `edit` are allowed globally (create asks first), so for this loop the rule
|
||||
above is the only thing holding them back.
|
||||
- Act only on the target PR. Don't touch unrelated files or other PRs.
|
||||
- **Keep context lean:** no raw JSON dumps in replies, no re-listing threads that haven't changed,
|
||||
idle cycles ≤2 lines. Every line you emit is re-read (uncached) on every later cycle.
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
"""Claude Code status line.
|
||||
|
||||
One line: vim mode | dir (repo-relative, hidden at the root) |
|
||||
git(repo/branch +dirty +ahead/behind) | model (+effort) |
|
||||
git(repo/branch +dirty +ahead/behind) | PR review threads (open count, Codex's share) | model (+effort) |
|
||||
context% (+compact warn) | 5h usage (+reset eta) | cost (+burn rate) |
|
||||
session tokens (cumulative in/out) | prompt-cache hit% (+ttl, cold marker) | disk. If the
|
||||
session tokens (cumulative in/out) | decode speed t/s (thinking blocks) |
|
||||
prompt-cache hit% (+ttl, cold marker) | disk. If the
|
||||
rendered line would overflow the terminal, trailing (lowest-priority) segments are
|
||||
dropped until it fits.
|
||||
(Dormant, re-addable in main(): ram, cpu%, temp -- retired 2026-07-26 because the
|
||||
status line only refreshes on message events, so system metrics sat visibly stale;
|
||||
status line then only refreshed on message events, so system metrics sat visibly stale
|
||||
(settings.json's 60 s refreshInterval, added for the PR count, is still too coarse for them);
|
||||
tmux's status bar owns those now -- plus velocity, cache hit %, api ratio, version,
|
||||
style.)
|
||||
|
||||
@@ -16,6 +18,7 @@ Reads the status JSON from stdin (Claude Code statusLine command). Every segment
|
||||
is wrapped defensively so the status line can never crash the UI -- on any error
|
||||
a segment is simply omitted.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -159,6 +162,9 @@ def last_usage(data):
|
||||
# --- session token totals (transcript, incremental) ---------------------------
|
||||
TOKENS_STATE_DIR = os.path.expanduser("~/.claude/cache")
|
||||
TOKENS_RECENT_IDS = 20 # message ids remembered across runs for de-duplication
|
||||
TPS_SAMPLES = 8 # thinking blocks the t/s readout is averaged over
|
||||
TPS_MIN_MS, TPS_MIN_TOKENS = 1000, 50 # shorter blocks are first-token latency, not decode speed
|
||||
TOKENS_SCAN_BUDGET = 8 << 20 # bytes parsed per refresh (~0.1 s); see session_tokens
|
||||
TOKENS_STATE_MAX_AGE = 7 * 86400 # prune per-session state files older than this
|
||||
|
||||
|
||||
@@ -183,9 +189,10 @@ def _tokens_state_path(data, path):
|
||||
|
||||
|
||||
def _prune_tokens_state(keep):
|
||||
"""Drop stale per-session state files (one scandir, only when the dir has piled up)."""
|
||||
"""Drop stale per-session token and per-branch PR state files (one scandir, only when
|
||||
the dir has piled up)."""
|
||||
try:
|
||||
entries = [e for e in os.scandir(TOKENS_STATE_DIR) if e.name.startswith("statusline.tokens.")]
|
||||
entries = [e for e in os.scandir(TOKENS_STATE_DIR) if e.name.startswith(("statusline.tokens.", "statusline.pr."))]
|
||||
if len(entries) < 50:
|
||||
return
|
||||
cutoff = time.time() - TOKENS_STATE_MAX_AGE
|
||||
@@ -197,12 +204,21 @@ def _prune_tokens_state(keep):
|
||||
|
||||
|
||||
def session_tokens(data):
|
||||
"""Cumulative (input, output) tokens this session, summed over the transcript's
|
||||
assistant `message.usage` blocks -- or None when there is no transcript.
|
||||
"""Cumulative session token totals plus decode-speed samples, from the transcript --
|
||||
`{"in", "out", "tps"}` or None when there is no transcript.
|
||||
|
||||
Input = input + cache_creation + cache_read (everything the API billed as input), so
|
||||
on a 1M session it dwarfs output: every call re-reads the whole cached prefix.
|
||||
Only the main transcript is counted; subagent usage lives in separate sidechain files.
|
||||
in/out: summed over assistant `message.usage` blocks. Input = input + cache_creation +
|
||||
cache_read (everything the API billed as input), so on a 1M session it dwarfs output:
|
||||
every call re-reads the whole cached prefix. Only the main transcript is counted;
|
||||
subagent usage lives in separate sidechain files.
|
||||
|
||||
tps: the last TPS_SAMPLES `[thinking_tokens, thinkingDurationMs]` pairs -- the only
|
||||
clean per-call timing Claude Code records. The status payload's cost.total_api_duration_ms
|
||||
is useless for a rate: subagent and advisor calls land in the same counter (it grew 28s
|
||||
in a 12s wall interval with no main-conversation completion), and the payload's
|
||||
output_tokens is sometimes a mid-stream partial. thinkingDurationMs is main-conversation
|
||||
only; it includes first-token latency, so short blocks read low and long ones converge
|
||||
on the real decode speed -- hence the duration-weighted mean and the minimum-size filter.
|
||||
|
||||
Incremental: the status line runs on every refresh and transcripts grow to tens of
|
||||
MB, so a per-session state file (~/.claude/cache/statusline.tokens.<session>.json)
|
||||
@@ -210,12 +226,18 @@ def session_tokens(data):
|
||||
de-duplicated by message id: the transcript writes one line per content block of a
|
||||
turn, all carrying the same id and the same usage block. Only complete lines are
|
||||
consumed (the writer may be mid-line); a shrunken file (truncate/rotate) resets.
|
||||
|
||||
A cold scan is spread over refreshes, TOKENS_SCAN_BUDGET bytes each: Claude Code kills
|
||||
a status script when the next refresh fires, and a multi-second scan of a 100 MB
|
||||
transcript that never gets to write its state would start from zero every time.
|
||||
Totals climb for a few refreshes on a huge resumed transcript, then settle.
|
||||
"""
|
||||
path = data.get("transcript_path")
|
||||
if not path or not os.path.exists(path):
|
||||
return None
|
||||
state_path = _tokens_state_path(data, path)
|
||||
state = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": []}
|
||||
fresh = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": [], "tps": [], "tps_ids": []}
|
||||
state = dict(fresh)
|
||||
try:
|
||||
with open(state_path) as fh:
|
||||
s = json.load(fh)
|
||||
@@ -225,27 +247,36 @@ def session_tokens(data):
|
||||
and all(isinstance(s.get(k), int) and s[k] >= 0 for k in ("offset", "in", "out"))
|
||||
and isinstance(s.get("ids"), list)
|
||||
):
|
||||
state = s
|
||||
state = s # "tps"/"tps_ids" are optional: older state files predate them
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def result(st):
|
||||
return {"in": st["in"], "out": st["out"], "tps": st.get("tps") or []}
|
||||
|
||||
size = os.path.getsize(path)
|
||||
if size < state["offset"]: # truncated or rotated under us: start over
|
||||
state = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": []}
|
||||
state = dict(fresh)
|
||||
if size == state["offset"]:
|
||||
return state["in"], state["out"]
|
||||
return result(state)
|
||||
|
||||
with open(path, "rb") as fh:
|
||||
fh.seek(state["offset"])
|
||||
chunk = fh.read()
|
||||
end = chunk.rfind(b"\n")
|
||||
chunk = fh.read(TOKENS_SCAN_BUDGET)
|
||||
end = chunk.rfind(b"\n")
|
||||
if end < 0 and len(chunk) == TOKENS_SCAN_BUDGET: # one line bigger than the budget: finish it
|
||||
chunk += fh.readline()
|
||||
end = chunk.rfind(b"\n")
|
||||
if end < 0: # no complete new line yet
|
||||
return state["in"], state["out"]
|
||||
return result(state)
|
||||
text = chunk[: end + 1].decode("utf-8", "ignore")
|
||||
|
||||
tin, tout = state["in"], state["out"]
|
||||
ids = [i for i in state["ids"] if isinstance(i, str)]
|
||||
seen = set(ids)
|
||||
tps = [s for s in (state.get("tps") or []) if isinstance(s, list) and len(s) == 2]
|
||||
tps_ids = [i for i in (state.get("tps_ids") or []) if isinstance(i, str)]
|
||||
tps_seen = set(tps_ids)
|
||||
for line in text.splitlines():
|
||||
if '"usage"' not in line:
|
||||
continue
|
||||
@@ -258,19 +289,38 @@ def session_tokens(data):
|
||||
if not isinstance(usage, dict):
|
||||
continue
|
||||
mid = msg.get("id") or obj.get("requestId") or obj.get("uuid")
|
||||
if not isinstance(mid, str) or mid in seen:
|
||||
if not isinstance(mid, str):
|
||||
continue
|
||||
|
||||
def num(d, k):
|
||||
v = d.get(k, 0) if isinstance(d, dict) else 0
|
||||
return int(v) if isinstance(v, (int, float)) else 0
|
||||
|
||||
# Decode-speed sample: checked on every line (not just the first-wins one below) so
|
||||
# it does not depend on the thinking block being the message's first line.
|
||||
ms = num(obj, "thinkingDurationMs")
|
||||
toks = num(usage.get("output_tokens_details"), "thinking_tokens")
|
||||
if ms >= TPS_MIN_MS and toks >= TPS_MIN_TOKENS and mid not in tps_seen:
|
||||
tps_seen.add(mid)
|
||||
tps_ids.append(mid)
|
||||
tps.append([toks, ms])
|
||||
|
||||
if mid in seen:
|
||||
continue
|
||||
seen.add(mid)
|
||||
ids.append(mid)
|
||||
tin += num(usage, "input_tokens") + num(usage, "cache_creation_input_tokens") + num(usage, "cache_read_input_tokens")
|
||||
tout += num(usage, "output_tokens")
|
||||
|
||||
def num(k):
|
||||
v = usage.get(k, 0)
|
||||
return int(v) if isinstance(v, (int, float)) else 0
|
||||
|
||||
tin += num("input_tokens") + num("cache_creation_input_tokens") + num("cache_read_input_tokens")
|
||||
tout += num("output_tokens")
|
||||
|
||||
state = {"path": path, "offset": state["offset"] + end + 1, "in": tin, "out": tout, "ids": ids[-TOKENS_RECENT_IDS:]}
|
||||
state = {
|
||||
"path": path,
|
||||
"offset": state["offset"] + end + 1,
|
||||
"in": tin,
|
||||
"out": tout,
|
||||
"ids": ids[-TOKENS_RECENT_IDS:],
|
||||
"tps": tps[-TPS_SAMPLES:],
|
||||
"tps_ids": tps_ids[-TOKENS_RECENT_IDS:],
|
||||
}
|
||||
try:
|
||||
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
|
||||
tmp = f"{state_path}.{os.getpid()}.tmp"
|
||||
@@ -280,7 +330,7 @@ def session_tokens(data):
|
||||
_prune_tokens_state(state_path)
|
||||
except Exception:
|
||||
pass
|
||||
return tin, tout
|
||||
return result(state)
|
||||
|
||||
|
||||
# --- row 1: work ------------------------------------------------------------
|
||||
@@ -395,6 +445,122 @@ def git_segment(cwd, top):
|
||||
return ""
|
||||
|
||||
|
||||
# --- PR review threads (gh, cached, fetched in the background) ------------------
|
||||
PR_TTL = 90 # seconds a fetched count stays fresh
|
||||
PR_ERR_TTL = 600 # back-off after a failed fetch (no GitHub remote, gh not authed, offline)
|
||||
PR_CLAIM = 30 # a refresher spawned this recently is still running: don't start another
|
||||
PR_CODEX = re.compile(r"codex", re.I) # chatgpt-codex-connector; same match as /pr-loop
|
||||
PR_QUERY = """query($owner:String!,$repo:String!,$branch:String!){
|
||||
repository(owner:$owner,name:$repo){
|
||||
pullRequests(headRefName:$branch,states:OPEN,first:1,orderBy:{field:UPDATED_AT,direction:DESC}){
|
||||
nodes{ number reviewThreads(first:100){ pageInfo{hasNextPage}
|
||||
nodes{ isResolved comments(first:1){ nodes{ author{login} } } } } } } } }"""
|
||||
|
||||
|
||||
def _pr_cache_path(top, branch):
|
||||
key = hashlib.sha1(f"{top}\0{branch}".encode()).hexdigest()[:16]
|
||||
return os.path.join(TOKENS_STATE_DIR, f"statusline.pr.{key}.json")
|
||||
|
||||
|
||||
def _read_json(path):
|
||||
try:
|
||||
with open(path) as fh:
|
||||
d = json.load(fh)
|
||||
return d if isinstance(d, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def pr_refresh(cwd, branch, path):
|
||||
"""Background half of pr_segment (`statusline.py --pr-refresh CWD BRANCH CACHE`): fetch the
|
||||
branch's open PR and write its unresolved review-thread counts to the cache. `{owner}` /
|
||||
`{repo}` are gh placeholders resolved from the repo at `cwd`. A failed fetch keeps the last
|
||||
counts and flags them stale; a branch without an open PR clears them."""
|
||||
state = _read_json(path)
|
||||
state["ts"] = time.time()
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["gh", "api", "graphql", "-F", "owner={owner}", "-F", "repo={repo}",
|
||||
"-f", f"branch={branch}", "-f", f"query={PR_QUERY}"],
|
||||
cwd=cwd, capture_output=True, text=True, timeout=20,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(r.stderr)
|
||||
nodes = json.loads(r.stdout)["data"]["repository"]["pullRequests"]["nodes"]
|
||||
state = {"ts": state["ts"]}
|
||||
if nodes:
|
||||
threads = nodes[0]["reviewThreads"]
|
||||
# a thread belongs to whoever opened it: its first comment's author
|
||||
authors = [((t["comments"]["nodes"] or [{}])[0].get("author") or {}).get("login") or ""
|
||||
for t in threads["nodes"] if not t["isResolved"]]
|
||||
state.update(number=nodes[0]["number"], open=len(authors),
|
||||
codex=sum(1 for a in authors if PR_CODEX.search(a)),
|
||||
more=threads["pageInfo"]["hasNextPage"])
|
||||
except Exception:
|
||||
state["error"] = True
|
||||
try:
|
||||
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
|
||||
tmp = f"{path}.{os.getpid()}.tmp"
|
||||
with open(tmp, "w") as fh:
|
||||
json.dump(state, fh)
|
||||
os.replace(tmp, path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def pr_segment(cwd, top):
|
||||
"""Unresolved review threads (any author, outdated ones included) on the current branch's
|
||||
open PR: `💬 #N ✓` when none are left, else the count with Codex's share broken out.
|
||||
|
||||
A GitHub round trip takes ~0.7 s, too slow for a render, so this only reads a per-branch
|
||||
cache (~/.claude/cache/statusline.pr.<hash>.json) and, once it is older than PR_TTL,
|
||||
spawns a detached `--pr-refresh` child to update it -- the new count shows on the next
|
||||
render. PR-conversation comments are not counted: they have no resolved state, so a
|
||||
summary or an LGTM would keep the count from ever reaching zero.
|
||||
"""
|
||||
try:
|
||||
if not top or not shutil.which("gh"):
|
||||
return ""
|
||||
branch = _git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
|
||||
if not branch or branch == "HEAD":
|
||||
return ""
|
||||
path = _pr_cache_path(top, branch)
|
||||
state = _read_json(path)
|
||||
if time.time() - (state.get("ts") or 0) > (PR_ERR_TTL if state.get("error") else PR_TTL):
|
||||
claim = path + ".lock"
|
||||
try:
|
||||
claimed = time.time() - os.path.getmtime(claim) < PR_CLAIM
|
||||
except OSError:
|
||||
claimed = False
|
||||
if not claimed:
|
||||
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
|
||||
with open(claim, "w"):
|
||||
pass
|
||||
# own session + no inherited stdout: Claude Code reads ours to EOF, and
|
||||
# cancels an in-flight render when the next one triggers
|
||||
subprocess.Popen(
|
||||
[sys.executable, os.path.abspath(__file__), "--pr-refresh", cwd, branch, path],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
num = state.get("number")
|
||||
if not num:
|
||||
return ""
|
||||
n, codex = state.get("open") or 0, state.get("codex") or 0
|
||||
out = "💬 " + color(f"#{num} ", "gray")
|
||||
if not n:
|
||||
out += color("✓", "green")
|
||||
else:
|
||||
out += color(f"{n}{'+' if state.get('more') else ''}", "yellow", bold=True)
|
||||
if codex:
|
||||
out += color(" · ", "gray") + color(f"codex {codex}", "red")
|
||||
if state.get("error"):
|
||||
out += color(" ?", "gray") # last fetch failed: these counts are stale
|
||||
return out
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def model_segment(data):
|
||||
name = (data.get("model") or {}).get("display_name")
|
||||
if not name:
|
||||
@@ -503,13 +669,12 @@ def velocity_segment(data):
|
||||
return ""
|
||||
|
||||
|
||||
def tokens_segment(data):
|
||||
def tokens_segment(totals):
|
||||
"""Cumulative session tokens: ↑ input (incl. cache reads/writes) and ↓ output."""
|
||||
try:
|
||||
totals = session_tokens(data)
|
||||
if not totals:
|
||||
return ""
|
||||
tin, tout = totals
|
||||
tin, tout = totals["in"], totals["out"]
|
||||
if not (tin or tout):
|
||||
return ""
|
||||
return "🔢 " + color(f"↑{fmt_tokens(tin)}", "cyan") + " " + color(f"↓{fmt_tokens(tout)}", "green")
|
||||
@@ -517,6 +682,22 @@ def tokens_segment(data):
|
||||
return ""
|
||||
|
||||
|
||||
def tps_segment(totals):
|
||||
"""Decode speed in tokens/s: duration-weighted mean over the last few thinking blocks
|
||||
(see session_tokens). Omitted until a thinking block ≥1s has completed -- so never
|
||||
shown with thinking disabled."""
|
||||
try:
|
||||
samples = (totals or {}).get("tps") or []
|
||||
toks = sum(s[0] for s in samples)
|
||||
ms = sum(s[1] for s in samples)
|
||||
if toks <= 0 or ms <= 0:
|
||||
return ""
|
||||
rate = toks / ms * 1000
|
||||
return "⚡ " + color(f"{rate:.0f} t/s", bucket(rate, 20, 40, invert=True))
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def cache_segment(usage, data=None):
|
||||
"""Prompt-cache hit % (higher is better) + the cache TTL, `·cold` when the cache has
|
||||
expired. Hit % from the 2.1.260+ payload `prompt_cache.hit_ratio` (a 0..1 fraction, null
|
||||
@@ -697,6 +878,9 @@ def style_segment(data):
|
||||
|
||||
|
||||
def main():
|
||||
if sys.argv[1:2] == ["--pr-refresh"] and len(sys.argv) == 5:
|
||||
pr_refresh(*sys.argv[2:])
|
||||
return
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except Exception:
|
||||
@@ -708,6 +892,7 @@ def main():
|
||||
or os.getcwd()
|
||||
)
|
||||
usage = payload_usage(data) or last_usage(data)
|
||||
totals = session_tokens(data)
|
||||
top = git_top(cwd)
|
||||
|
||||
# One row: work (left) then system (right), joined. If it would overflow the
|
||||
@@ -720,11 +905,13 @@ def main():
|
||||
vim_segment(data),
|
||||
dir_segment(cwd, top),
|
||||
git_segment(cwd, top),
|
||||
pr_segment(cwd, top),
|
||||
model_segment(data),
|
||||
context_segment(data, usage),
|
||||
usage_segment(data),
|
||||
cost_segment(data),
|
||||
tokens_segment(data),
|
||||
tokens_segment(totals),
|
||||
tps_segment(totals),
|
||||
cache_segment(usage, data),
|
||||
) if s]
|
||||
system = [s for s in (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
|
||||
"logo": {
|
||||
"source": "$(find \"${XDG_CONFIG_HOME:-$HOME/.config}/fastfetch/pngs/\" -name \"*.png\" | shuf -n 1)",
|
||||
"source": "$(find \"${XDG_CONFIG_HOME:-$HOME/.config}/fastfetch/pngs/\" -name \"*.png\" -xtype f | shuf -n 1)",
|
||||
"height": 18
|
||||
},
|
||||
"display": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,27 @@
|
||||
# Mark the tmux window when a long shell command finishes, through the same
|
||||
# ~/.config/tmux/scripts/agent-state.sh the coding agents use (tool tag "sh"):
|
||||
# ✓ on exit 0, ! on failure — coloured when it happened while you were elsewhere, grey once
|
||||
# visited, gone at the next command. Only commands longer than $agent_state_min_ms
|
||||
# (default 30 s) count, and interactive programs are skipped: the agents mark themselves,
|
||||
# and quitting a long nvim or lazygit session is not news. No-op outside tmux.
|
||||
set -q agent_state_min_ms; or set -g agent_state_min_ms 30000
|
||||
|
||||
function __agent_state_preexec --on-event fish_preexec
|
||||
set -q TMUX_PANE; or return
|
||||
~/.config/tmux/scripts/agent-state.sh sh busy
|
||||
end
|
||||
|
||||
function __agent_state_postexec --on-event fish_postexec
|
||||
set -l st $status # first statement: anything before it would clobber the command's status
|
||||
set -q TMUX_PANE; or return
|
||||
test "$CMD_DURATION" -gt "$agent_state_min_ms"; or return
|
||||
switch (string split -m1 ' ' -- $argv[1])[1]
|
||||
case nvim vim vi less man lazygit ssh mosh tmux claude claudes pi opencode opencodes codex aichat htop btop top journalctl tail watch
|
||||
return
|
||||
end
|
||||
if test $st -eq 0
|
||||
~/.config/tmux/scripts/agent-state.sh sh done
|
||||
else
|
||||
~/.config/tmux/scripts/agent-state.sh sh error
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
# Drop kitty's KITTY_PUBLIC_KEY from the shell environment.
|
||||
#
|
||||
# kitty generates a random 40-char base85 public key per instance and exports it
|
||||
# to every child (kitty/child.py). It is only read on the password path of kitty
|
||||
# remote control (kitty/remote_control.py: "Password usage requested but
|
||||
# KITTY_PUBLIC_KEY environment variable is not available"), which needs
|
||||
# allow_remote_control — not enabled here. `kitten ssh`, icat, clipboard and
|
||||
# shell integration never touch it.
|
||||
#
|
||||
# Why bother: tools that inline the environment into generated code get corrupted
|
||||
# when a value contains a String.replace replacement sequence. prettier's
|
||||
# --experimental-cli inlines JSON.stringify(process.env) into its parallel worker
|
||||
# and splices it with .replace(placeholder, bootloader); a `$` + backtick in the
|
||||
# key expands to "everything before the match", injecting the worker's own source
|
||||
# mid-string. Every file then fails with a bogus
|
||||
# "SyntaxError: Unexpected identifier 'string'". Roughly a 1-in-185 chance per
|
||||
# kitty launch, which is exactly as fun to debug as it sounds.
|
||||
if set -q KITTY_PUBLIC_KEY
|
||||
set -e KITTY_PUBLIC_KEY
|
||||
end
|
||||
@@ -106,7 +106,7 @@ if status is-interactive
|
||||
abbr scrcpys 'scrcpy -wS --power-off-on-close'
|
||||
abbr tts "tt -notheme -bold -showwpm -json"
|
||||
abbr tuioss "tuios --show-clock --show-keys --show-cpu --show-ram --confirm-quit"
|
||||
abbr keybinds "cd ~/.dots/ && nvim gui/.config/hypr/keybindings.conf"
|
||||
abbr keybinds "cd ~/.dots/ && nvim gui/.config/hypr/dots_binds.lua"
|
||||
abbr vpnhome "sudo wg-quick up wg0"
|
||||
abbr vpnkralizec "sudo wg-quick up kralizec-wg0"
|
||||
abbr vpnsumadija "sudo wg-quick up sumadija-wg0"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
function glow --wraps glow --description 'glow wrapped at the current terminal width; an explicit -w still wins'
|
||||
# glow never sizes itself to the terminal: glow.yml's width (or its built-in 80) applies, and
|
||||
# `width: 0` in the config means 80 too. Wide tables get squeezed into that, one word per line.
|
||||
# $COLUMNS tracks window resizes; a later -w/--width in $argv overrides this one.
|
||||
command glow --width $COLUMNS $argv
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
function md2pdf --description 'Markdown to PDF next to the source: cmark-gfm renders it, headless chromium prints it'
|
||||
# Relative image paths don't resolve (the HTML is rendered from /tmp), and chromium
|
||||
# leaves out backgrounds when printing, so code blocks lose their grey box.
|
||||
set -l html (mktemp --suffix=.html)
|
||||
begin
|
||||
echo '<meta charset="utf-8"><style>body{font-family:sans-serif;max-width:46em;margin:auto;line-height:1.5}table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:.3em .6em}</style>'
|
||||
cmark-gfm -e table -e strikethrough -e autolink -e tasklist $argv[1]
|
||||
end >$html
|
||||
chromium --headless --no-pdf-header-footer --print-to-pdf=(path change-extension pdf $argv[1]) $html 2>/dev/null
|
||||
rm $html
|
||||
end
|
||||
@@ -35,4 +35,4 @@ traceroute
|
||||
fuck
|
||||
atool
|
||||
aria2c
|
||||
|
||||
cmark-gfm
|
||||
@@ -1,31 +1,31 @@
|
||||
{
|
||||
"LuaSnip": { "branch": "master", "commit": "0abc8f390b278c3b4aabc4c004ac8a088b65cf24" },
|
||||
"NvChad": { "branch": "v2.5", "commit": "add44b952d631981614bbb8cfc6f7002f296dfe6" },
|
||||
"base46": { "branch": "v3.0", "commit": "267954c8663607823f03a3259bb8deb15688212f" },
|
||||
"NvChad": { "branch": "v2.5", "commit": "87d6a41ec07ab995de912a892749b6b4805ad2b3" },
|
||||
"base46": { "branch": "v3.0", "commit": "d870ec89a7b2f28c4accce515f97b7bcc5b9d200" },
|
||||
"cmp-async-path": { "branch": "main", "commit": "98185a91d49ff5dd249aebf2f7456e18063fa2a0" },
|
||||
"cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" },
|
||||
"cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" },
|
||||
"cmp-nvim-lua": { "branch": "main", "commit": "e3a22cb071eb9d6508a156306b102c45cd2d573d" },
|
||||
"cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" },
|
||||
"conform.nvim": { "branch": "master", "commit": "016802de402556da54c36bd7359b441266b01cdd" },
|
||||
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" },
|
||||
"gitsigns.nvim": { "branch": "main", "commit": "d29ecf6626cf6ffb109fb04a8ecd92c46f4a1ce9" },
|
||||
"friendly-snippets": { "branch": "main", "commit": "b2ea5c41a150ba72fb656eedb4ed0a86d138d945" },
|
||||
"gitsigns.nvim": { "branch": "main", "commit": "4bcd99167c265f135886b282baad77afe06206b2" },
|
||||
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
|
||||
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
|
||||
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
|
||||
"menu": { "branch": "main", "commit": "7a0a4a2896b715c066cfbe320bdc048091874cc6" },
|
||||
"minty": { "branch": "main", "commit": "aafc9e8e0afe6bf57580858a2849578d8d8db9e0" },
|
||||
"nvim-autopairs": { "branch": "master", "commit": "7b9923abad60b903ece7c52940e1321d39eccc79" },
|
||||
"nvim-cmp": { "branch": "main", "commit": "2ffe79f1f021def8dd1fcd81deb16f1bb0d989f3" },
|
||||
"nvim-lspconfig": { "branch": "master", "commit": "4267d26e60323a45e18f8e7a63a964cc8f53ca4e" },
|
||||
"nvim-tree.lua": { "branch": "master", "commit": "b2aadda94b107480c48e548d6db51c6840b7b33c" },
|
||||
"nvim-treesitter": { "branch": "main", "commit": "c9f9ed6c1892f629ea399f4ee7905f2686fa13f2" },
|
||||
"nvim-web-devicons": { "branch": "master", "commit": "2ae6958df7ced50baac5035cec0c15799eedfbf7" },
|
||||
"nvim-autopairs": { "branch": "master", "commit": "3172f40c16563db871a58f20d205c710ad782311" },
|
||||
"nvim-cmp": { "branch": "main", "commit": "e04302f1fefcb5653848346eb3723b39b82c549b" },
|
||||
"nvim-lspconfig": { "branch": "master", "commit": "cc404d425c7752f56b8eaa269107be44d888ea51" },
|
||||
"nvim-tree.lua": { "branch": "master", "commit": "69eb0c34ed3d836d5a0d92d913b77dd73b054f2f" },
|
||||
"nvim-treesitter": { "branch": "main", "commit": "610734c5d3e59761d5f79318d120633374537fa5" },
|
||||
"nvim-web-devicons": { "branch": "master", "commit": "ec309a0eaff6ea967e58c275996155ab5c16938d" },
|
||||
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
|
||||
"render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" },
|
||||
"render-markdown.nvim": { "branch": "main", "commit": "8e8795368391036643b622875a6da6a51bcd33f4" },
|
||||
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
|
||||
"telescope.nvim": { "branch": "master", "commit": "427b576c16792edad01a92b89721d923c19ad60f" },
|
||||
"ui": { "branch": "v3.0", "commit": "fe781d1c12860d6a25d45e588fe4fdd27eb34a1a" },
|
||||
"volt": { "branch": "main", "commit": "620de1321f275ec9d80028c68d1b88b409c0c8b1" },
|
||||
"telescope.nvim": { "branch": "master", "commit": "40aedd8a68c78a656a10a8d62d80c54af59420fb" },
|
||||
"ui": { "branch": "v3.0", "commit": "8cbf0dc1883b20a1df114149c187d250f0af0853" },
|
||||
"volt": { "branch": "main", "commit": "4351c5cd790a0d39491c4d5c75dd62d7db3d3414" },
|
||||
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
|
||||
}
|
||||
@@ -15,6 +15,7 @@ service caches config).
|
||||
| `agents/*.md` | Four subagents with real prompts and scoped permissions: `code-reviewer`, `test-runner` (hidden), `doc-writer` (edits docs only), `security-auditor` (hidden). |
|
||||
| `commands/*.md` | `/commit` (Conventional Commits message, never commits) and `/review` (read-only diff review), both on the `plan` agent. |
|
||||
| `plugins/verbose/` | 2.x TUI plugin: `<leader>v`, `/verbose`, or the palette entry toggles the *verbose view* — thinking blocks expanded and tool calls ungrouped — reading the live state from the built-in toggles so a mixed state converges. `server.ts` is a no-op half required by the loader. |
|
||||
| `plugins/agent-state.ts` | Server-side plugin: mirrors the session state (`message.updated` user → busy, `permission.asked` → input, `session.idle` → done, `session.error` → error) into the tmux pane opencode runs in via `~/.config/tmux/scripts/agent-state.sh`, for the status-bar agent badges. Skips child sessions; only right when the TUI in the pane spawned the server (not `serve`/`attach`); under `opencodes` (bwrap) the tmux socket dir must be bound in. |
|
||||
| `themes/catppuccin-mocha.json` | Catppuccin Mocha (dark) / Latte (light) theme following the pi theme's semantic mapping. Built-in `catppuccin` exists too; this one matches pi. |
|
||||
| `tui.json` | TUI settings in the v1 schema (2.x compat): theme, leader timeout, stacked diffs, block cursor, attention notifications, a few extra keybinds. |
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s, remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
|
||||
"GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s, remote)", "limit": { "context": 24576, "output": 8192 } },
|
||||
"GLM-4.7-Flash-UD-Q3_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash Q3 · 24k — fast coder (~28 t/s, remote)", "limit": { "context": 24576, "output": 8192 } },
|
||||
"Ornith-1.5-35B-A3B-IQ3_XXS": { "reasoning": true, "name": "Ornith 1.5 35B · 24k · vision — agentic coder, thinking (~32 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
|
||||
"Ornith-1.5-35B-A3B-IQ3_XXS": { "reasoning": true, "name": "Ornith 1.5 35B · 32k · vision — agentic coder, thinking (~32 t/s, remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
|
||||
"gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s, remote)", "limit": { "context": 65536, "output": 8192 } },
|
||||
"gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s, remote)", "limit": { "context": 65536, "output": 8192 } }
|
||||
}
|
||||
@@ -174,7 +174,7 @@
|
||||
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
|
||||
"GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)", "limit": { "context": 24576, "output": 8192 } },
|
||||
"GLM-4.7-Flash-UD-Q3_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash Q3 · 24k — fast coder (~28 t/s)", "limit": { "context": 24576, "output": 8192 } },
|
||||
"Ornith-1.5-35B-A3B-IQ3_XXS": { "reasoning": true, "name": "Ornith 1.5 35B · 24k · vision — agentic coder, thinking (~32 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
|
||||
"Ornith-1.5-35B-A3B-IQ3_XXS": { "reasoning": true, "name": "Ornith 1.5 35B · 32k · vision — agentic coder, thinking (~32 t/s)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
|
||||
"gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)", "limit": { "context": 65536, "output": 8192 } },
|
||||
"gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)", "limit": { "context": 65536, "output": 8192 } }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// opencode agent-state plugin: mirrors the session state into the tmux pane opencode runs in via
|
||||
// ~/.config/tmux/scripts/agent-state.sh (oc busy|done|input|error). No-op outside tmux.
|
||||
//
|
||||
// Runs in the SERVER process, so TMUX_PANE is only right when the TUI in the pane spawned the
|
||||
// server (plain `opencode`), not for `opencode serve` / `opencode attach`. Under the bwrap
|
||||
// `opencodes` wrapper the tmux socket dir must be bound in, or the script gives up quietly.
|
||||
// Child (subagent) sessions are skipped so a subagent finishing does not flag "done".
|
||||
import type { Plugin } from "@opencode-ai/plugin"
|
||||
|
||||
export const AgentState: Plugin = async ({ client, $ }) => {
|
||||
const script = `${process.env.HOME}/.config/tmux/scripts/agent-state.sh`
|
||||
const set = async (state: string) => {
|
||||
if (!process.env.TMUX_PANE) return
|
||||
try {
|
||||
await $`${script} oc ${state}`.quiet().nothrow()
|
||||
} catch {}
|
||||
}
|
||||
const childCache = new Map<string, boolean>()
|
||||
const isChild = async (sessionID?: string) => {
|
||||
if (!sessionID) return false
|
||||
if (!childCache.has(sessionID)) {
|
||||
try {
|
||||
const r = await client.session.get({ path: { id: sessionID } })
|
||||
childCache.set(sessionID, Boolean(r.data?.parentID))
|
||||
} catch {
|
||||
childCache.set(sessionID, false)
|
||||
}
|
||||
}
|
||||
return childCache.get(sessionID) ?? false
|
||||
}
|
||||
return {
|
||||
event: async ({ event }) => {
|
||||
const p = (event as { properties?: Record<string, any> }).properties ?? {}
|
||||
switch (event.type) {
|
||||
case "message.updated": // a new user message = a prompt went in
|
||||
if (p.info?.role === "user" && !(await isChild(p.info?.sessionID))) await set("busy")
|
||||
break
|
||||
case "permission.asked":
|
||||
if (!(await isChild(p.sessionID))) await set("input")
|
||||
break
|
||||
case "permission.replied":
|
||||
if (!(await isChild(p.sessionID))) await set("busy")
|
||||
break
|
||||
case "session.idle":
|
||||
if (!(await isChild(p.sessionID))) await set("done")
|
||||
break
|
||||
case "session.error":
|
||||
if (!(await isChild(p.sessionID))) await set("error")
|
||||
break
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# agent-next.sh: switch to the next window (after the current one, wrapping) that holds an
|
||||
# agent mark you have not looked at yet; with none left, fall back to any grey mark; with
|
||||
# nothing marked, say so. Bound to prefix+Tab and to the » tap target in the status bar.
|
||||
cur=$(tmux display -p '#{window_index}')
|
||||
pick() { # $1 empty = unseen marks only, "any" = grey ones too
|
||||
tmux list-panes -a -F '#{window_index} #{@agent} #{@agent_seen}' |
|
||||
awk -v c="$cur" -v any="$1" '
|
||||
$2 != "" && $2 != "busy" && (any != "" || $3 == "") { if ($1 > c && !n) n = $1; if (!f) f = $1 }
|
||||
END { if (n) print n; else if (f) print f }'
|
||||
}
|
||||
w=$(pick); [ -n "$w" ] || w=$(pick any)
|
||||
if [ -n "$w" ]; then tmux select-window -t ":$w"; else tmux display-message "no agent waiting"; fi
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
# agent-state.sh <cc|oc|pi|cx> <busy|done|input|error|off>
|
||||
#
|
||||
# Per-pane "what is the agent in this pane doing" for tmux, shared by every agent's hooks:
|
||||
# Claude Code (claude/settings.json), codex (~/.codex/hooks.json), pi (agent-state.ts
|
||||
# extension) and opencode (plugins/agent-state.ts). No-op outside tmux.
|
||||
#
|
||||
# Which pane: $TMUX_PANE when the agent runs in the pane itself (pi, opencode, codex). Claude
|
||||
# Code sessions run in a worker the claude daemon pre-spawned ("claude bg-spare"), whose
|
||||
# environment has no tmux variables, so for them the worker is found by walking up the parent
|
||||
# chain to the pid registered in ~/.claude/sessions/<pid>.json, and the pane is the one whose
|
||||
# title Claude set to that session's name ("✳ <name>").
|
||||
#
|
||||
# Pane options read by tmux.conf's @agent_badge: @agent (the state, drawn as ✓ ? ! after the
|
||||
# window name; busy draws nothing), @agent_seen (set by tmux's pane-focus-in hook: the glyph
|
||||
# loses its colour once you visit the pane and stays, grey, until the next prompt turns the
|
||||
# state busy) and @agent_tool (only the termux notification title uses it). `off` unsets all.
|
||||
#
|
||||
# Phone-level notice only for a NEW mark on a pane that is not in view: termux-notification
|
||||
# when the agent runs natively in termux, otherwise a BEL into the pane (tmux forwards the bell
|
||||
# to the outer terminal; termux's default bell-character=vibrate buzzes, and the window gets
|
||||
# tmux's bell flag). A state change on an already-marked pane (e.g. Claude's idle_prompt ~60s
|
||||
# after Stop turning done into input) only swaps the glyph: no second buzz, seen stays as is.
|
||||
command -v tmux >/dev/null 2>&1 || exit 0
|
||||
tool=$1 state=$2 pane=$TMUX_PANE
|
||||
if [ -z "$pane" ]; then
|
||||
p=$$ worker=""
|
||||
for _ in 1 2 3 4 5 6 7 8; do
|
||||
p=$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')
|
||||
[ -n "$p" ] && [ "$p" != 1 ] || break
|
||||
[ -f "$HOME/.claude/sessions/$p.json" ] && { worker=$p; break; }
|
||||
done
|
||||
[ -n "$worker" ] || exit 0
|
||||
name=$(python3 -I -c 'import json,sys; print(json.load(open(sys.argv[1])).get("name",""))' \
|
||||
"$HOME/.claude/sessions/$worker.json" 2>/dev/null)
|
||||
[ -n "$name" ] || exit 0
|
||||
pane=$(tmux list-panes -a -F '#{pane_id} #{pane_title}' 2>/dev/null |
|
||||
awk -F'\t' -v n="$name" '$2==n || substr($2,length($2)-length(n))==" " n {print $1; exit}')
|
||||
[ -n "$pane" ] || exit 0
|
||||
fi
|
||||
if [ "$state" = off ]; then
|
||||
for o in @agent @agent_seen @agent_tool; do tmux set -p -t "$pane" -u "$o" 2>/dev/null; done
|
||||
exit 0
|
||||
fi
|
||||
tmux set -p -t "$pane" @agent_tool "$tool" 2>/dev/null || exit 0 # no server reachable (sandbox): give up quietly
|
||||
case $state in
|
||||
busy)
|
||||
tmux set -p -t "$pane" @agent busy
|
||||
tmux set -p -t "$pane" -u @agent_seen 2>/dev/null
|
||||
command -v termux-notification-remove >/dev/null 2>&1 && termux-notification-remove "agent-$pane"
|
||||
;;
|
||||
*)
|
||||
already=$(tmux show -pv -t "$pane" @agent 2>/dev/null)
|
||||
tmux set -p -t "$pane" @agent "$state"
|
||||
if [ "$(tmux display -p -t "$pane" '#{pane_active}#{window_active}')" = 11 ]; then
|
||||
tmux set -p -t "$pane" @agent_seen 1 # in view: you saw it happen, so it starts out seen
|
||||
exit 0
|
||||
fi
|
||||
[ -n "$already" ] && [ "$already" != busy ] && exit 0 # glyph swapped on an existing mark
|
||||
tmux set -p -t "$pane" -u @agent_seen 2>/dev/null
|
||||
if command -v termux-notification >/dev/null 2>&1; then
|
||||
termux-notification --id "agent-$pane" --title "$tool: $state" --content "${PWD##*/}" \
|
||||
--vibrate 200 --action "tmux select-pane -t $pane"
|
||||
else
|
||||
printf '\a' > "$(tmux display -p -t "$pane" '#{pane_tty}')" 2>/dev/null
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# CPU utilisation (%) averaged over the interval since the PREVIOUS status refresh:
|
||||
# /proc/stat totals persist in a state file between runs (same pattern as net.sh),
|
||||
# so the value covers the whole status-interval. (The old version slept 0.3s and
|
||||
# sampled just that window — a 6% duty cycle that aliased bursty loads.) No sleep,
|
||||
# ~5ms. First run (no state yet) shows the since-boot average. Glyph = Waybar's
|
||||
# cpu module icon (U+F035B).
|
||||
printf '\U000f035b '
|
||||
rt="${XDG_RUNTIME_DIR:-/tmp}"
|
||||
st="$rt/tmux_cpu_stat" # previous totals: tot idle
|
||||
|
||||
read -r _ u n s i w rest </proc/stat
|
||||
idle=$((i + w))
|
||||
tot=$((u + n + s + i + w))
|
||||
for v in $rest; do tot=$((tot + v)); done
|
||||
|
||||
p_tot=0
|
||||
p_idle=0
|
||||
[ -r "$st" ] && read -r p_tot p_idle <"$st"
|
||||
[ "$p_tot" -gt "$tot" ] && {
|
||||
p_tot=0
|
||||
p_idle=0
|
||||
} # stale state from a previous boot
|
||||
printf '%s %s\n' "$tot" "$idle" >"$st"
|
||||
|
||||
dt=$((tot - p_tot))
|
||||
di=$((idle - p_idle))
|
||||
if [ "$dt" -le 0 ]; then printf -- '--%%'; else printf '%d%%' $(((100 * (dt - di) + dt / 2) / dt)); fi
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# RAM utilisation (%) — used = MemTotal - MemAvailable, from /proc/meminfo.
|
||||
# Called from tmux status-right via #(); cached per status-interval.
|
||||
printf '\U000f0f86 ' # Waybar's memory glyph (U+F0F86), then the percentage
|
||||
awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{if(t>0)printf "%d%%",(t-a)*100/t; else printf "--%%"}' /proc/meminfo
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
# metrics.sh full|compact — the status-right system segment in ONE process per refresh.
|
||||
# (tmux used to spawn net dl, net ul, cpu, mem and temp as five separate #() jobs per tick.)
|
||||
# Prints #[fg=…] style markers, which tmux honours inside #() output.
|
||||
# full : net dl · net ul · cpu · mem · temp (temp vanishes when no sensor exists)
|
||||
# compact : cpu · mem
|
||||
# CPU and net are averaged over the interval since the PREVIOUS refresh: the /proc totals
|
||||
# persist in $XDG_RUNTIME_DIR state files, so no sleeps and the value covers the whole
|
||||
# status-interval. First run (no state yet) shows the since-boot average. Glyphs are Waybar's:
|
||||
# cpu U+F035B, memory U+F0F86, thermometer U+F050F, download U+F019, upload U+F093.
|
||||
mode=${1:-full}
|
||||
rt="${XDG_RUNTIME_DIR:-/tmp}"
|
||||
|
||||
cpu() { # utilisation % since the previous refresh
|
||||
local st="$rt/tmux_cpu_stat" u n s i w rest idle tot v p_tot=0 p_idle=0 dt di
|
||||
read -r _ u n s i w rest </proc/stat
|
||||
idle=$((i + w))
|
||||
tot=$((u + n + s + i + w))
|
||||
for v in $rest; do tot=$((tot + v)); done
|
||||
[ -r "$st" ] && read -r p_tot p_idle <"$st"
|
||||
[ "$p_tot" -gt "$tot" ] && { p_tot=0; p_idle=0; } # stale state from a previous boot
|
||||
printf '%s %s\n' "$tot" "$idle" >"$st"
|
||||
dt=$((tot - p_tot))
|
||||
di=$((idle - p_idle))
|
||||
printf '\U000f035b '
|
||||
if [ "$dt" -le 0 ]; then printf -- '--%%'; else printf '%d%%' $(((100 * (dt - di) + dt / 2) / dt)); fi
|
||||
}
|
||||
|
||||
mem() { # used = MemTotal - MemAvailable
|
||||
printf '\U000f0f86 '
|
||||
awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{if(t>0)printf "%d%%",(t-a)*100/t; else printf "--%%"}' /proc/meminfo
|
||||
}
|
||||
|
||||
temp() { # CPU °C: a CPU-ish thermal zone, else a CPU hwmon chip, else the hottest zone; nothing if none
|
||||
local cpu_zone="" any_zone="" hw="" d t ty f
|
||||
for d in /sys/class/thermal/thermal_zone*/; do
|
||||
[ -r "${d}temp" ] && [ -r "${d}type" ] || continue
|
||||
t=$(<"${d}temp")
|
||||
case $t in '' | *[!0-9]*) continue ;; esac
|
||||
ty=$(<"${d}type"); ty=${ty,,}
|
||||
case $ty in *x86_pkg_temp* | *coretemp* | *k10temp* | *tctl* | *cpu*)
|
||||
if [ -z "$cpu_zone" ] || [ "$t" -gt "$cpu_zone" ]; then cpu_zone=$t; fi ;;
|
||||
esac
|
||||
if [ -z "$any_zone" ] || [ "$t" -gt "$any_zone" ]; then any_zone=$t; fi
|
||||
done
|
||||
if [ -z "$cpu_zone" ]; then
|
||||
for d in /sys/class/hwmon/hwmon*/; do
|
||||
[ -r "${d}name" ] || continue
|
||||
case $(<"${d}name") in coretemp | k10temp | zenpower | cpu_thermal)
|
||||
for f in "${d}"temp*_input; do
|
||||
[ -r "$f" ] || continue
|
||||
t=$(<"$f")
|
||||
case $t in '' | *[!0-9]*) continue ;; esac
|
||||
if [ -z "$hw" ] || [ "$t" -gt "$hw" ]; then hw=$t; fi
|
||||
done ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
t=${cpu_zone:-${hw:-$any_zone}}
|
||||
[ -n "$t" ] || return 0
|
||||
printf '\U000f050f %d° ' $(((t + 500) / 1000))
|
||||
}
|
||||
|
||||
human() { awk -v b="$1" 'BEGIN{ split("B K M G T", u, " "); i=1;
|
||||
while (b >= 1024 && i < 5) { b /= 1024; i++ }
|
||||
if (i == 1) printf "%d%s", b, u[i]; else printf "%.1f%s", b, u[i] }'; }
|
||||
|
||||
net() { # sets dl/ul: bytes per second over all non-lo interfaces since the previous refresh
|
||||
local ctr="$rt/tmux_net_ctr" now rx tx prx ptx pnow drx=0 dtx=0 dt=1
|
||||
now=$(date +%s.%N)
|
||||
read -r rx tx < <(awk 'NR>2{gsub(/:/," "); if($1!="lo"){r+=$2; t+=$10}} END{print r+0, t+0}' /proc/net/dev)
|
||||
if [ -r "$ctr" ]; then
|
||||
read -r prx ptx pnow <"$ctr"
|
||||
dt=$(awk -v a="$now" -v b="$pnow" 'BEGIN{d=a-b; print (d>0.05)?d:1}')
|
||||
drx=$((rx - prx)); dtx=$((tx - ptx))
|
||||
[ "$drx" -lt 0 ] && drx=0
|
||||
[ "$dtx" -lt 0 ] && dtx=0
|
||||
fi
|
||||
printf '%s %s %s\n' "$rx" "$tx" "$now" >"$ctr"
|
||||
dl=$(human "$(awk -v x="$drx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
|
||||
ul=$(human "$(awk -v x="$dtx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
|
||||
}
|
||||
|
||||
case $mode in
|
||||
compact)
|
||||
printf '#[fg=#89b4fa]%s #[fg=#94e2d5]%s' "$(cpu)" "$(mem)"
|
||||
;;
|
||||
*)
|
||||
net
|
||||
printf '#[fg=#a6e3a1]\U0000f019 %s #[fg=#f9e2af]\U0000f093 %s #[fg=#89b4fa]%s #[fg=#94e2d5]%s #[fg=#fab387]%s' \
|
||||
"$dl" "$ul" "$(cpu)" "$(mem)" "$(temp)"
|
||||
;;
|
||||
esac
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Network throughput from /proc/net/dev (sum of all non-lo interfaces), averaged
|
||||
# over the interval between status refreshes — no blocking sleep.
|
||||
#
|
||||
# Usage: net.sh dl -> " 1.2M" (download icon U+F019 + rate)
|
||||
# net.sh ul -> " 45K" (upload icon U+F093 + rate)
|
||||
#
|
||||
# tmux calls both from status-right. A short (<3s) cache lets the two calls of a
|
||||
# single refresh share one computation, so the up/down figures are consistent and
|
||||
# only one of them re-samples the counters per refresh.
|
||||
which=${1:-dl}
|
||||
rt="${XDG_RUNTIME_DIR:-/tmp}"
|
||||
ctr="$rt/tmux_net_ctr" # previous counters: rx tx epoch.ns
|
||||
cache="$rt/tmux_net_cache" # computed rates: dl_rate ul_rate epoch_s
|
||||
|
||||
emit() { # emit <icon-printf-escape> <rate>
|
||||
printf "$1 %s" "$2"
|
||||
}
|
||||
|
||||
# Reuse a fresh cache so dl and ul agree and we only sample once per refresh.
|
||||
fresh_emit() { # print from the cache and exit — if the cache is fresh enough
|
||||
[ -r "$cache" ] || return 1
|
||||
read -r cdl cul cts <"$cache"
|
||||
{ [ -n "$cts" ] && [ "$(($(date +%s) - cts))" -lt 3 ]; } || return 1
|
||||
if [ "$which" = ul ]; then emit '\U0000f093' "$cul"; else emit '\U0000f019' "$cdl"; fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
fresh_emit
|
||||
|
||||
# tmux launches the dl and ul jobs CONCURRENTLY each refresh, so without a lock
|
||||
# both miss the stale cache and compute independently — worst case the loser reads
|
||||
# the counters milliseconds after the winner rewrote them and shows a ~0 rate.
|
||||
# Serialize: the loser waits ~30ms on the lock, then hits the winner's fresh cache.
|
||||
exec 9>"$rt/tmux_net_lock"
|
||||
flock 9
|
||||
fresh_emit
|
||||
|
||||
now=$(date +%s.%N)
|
||||
read -r rx tx < <(awk 'NR>2{gsub(/:/," "); if($1!="lo"){r+=$2; t+=$10}} END{print r+0, t+0}' /proc/net/dev)
|
||||
|
||||
drx=0
|
||||
dtx=0
|
||||
dt=1
|
||||
if [ -r "$ctr" ]; then
|
||||
read -r prx ptx pnow <"$ctr"
|
||||
dt=$(awk -v a="$now" -v b="$pnow" 'BEGIN{d=a-b; print (d>0.05)?d:1}')
|
||||
drx=$((rx - prx))
|
||||
dtx=$((tx - ptx))
|
||||
[ "$drx" -lt 0 ] && drx=0
|
||||
[ "$dtx" -lt 0 ] && dtx=0
|
||||
fi
|
||||
printf '%s %s %s\n' "$rx" "$tx" "$now" >"$ctr"
|
||||
|
||||
human() { awk -v b="$1" 'BEGIN{ split("B K M G T", u, " "); i=1;
|
||||
while (b >= 1024 && i < 5) { b /= 1024; i++ }
|
||||
if (i == 1) printf "%d%s", b, u[i]; else printf "%.1f%s", b, u[i] }'; }
|
||||
|
||||
dl=$(human "$(awk -v x="$drx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
|
||||
ul=$(human "$(awk -v x="$dtx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
|
||||
printf '%s %s %s\n' "$dl" "$ul" "$(date +%s)" >"$cache"
|
||||
|
||||
if [ "$which" = ul ]; then emit '\U0000f093' "$ul"; else emit '\U0000f019' "$dl"; fi
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# CPU temperature (°C) for the tmux status bar (thermometer glyph U+F050F).
|
||||
# Sensor hunt, most→least specific: a CPU-ish thermal zone (x86_pkg_temp/…),
|
||||
# else a CPU hwmon chip (coretemp/k10temp/…), else the hottest remaining zone
|
||||
# (acpitz can be a bogus constant on desktops, hence last). Prints NOTHING when
|
||||
# no sensor exists (VMs/containers) so the segment simply disappears — which is
|
||||
# also why the trailing spacing lives in here, not in status-right.
|
||||
cpu_zone=""
|
||||
any_zone=""
|
||||
hw=""
|
||||
|
||||
for d in /sys/class/thermal/thermal_zone*/; do
|
||||
[ -r "${d}temp" ] && [ -r "${d}type" ] || continue
|
||||
t=$(<"${d}temp")
|
||||
case $t in '' | *[!0-9]*) continue ;; esac
|
||||
ty=$(<"${d}type")
|
||||
ty=${ty,,}
|
||||
case $ty in *x86_pkg_temp* | *coretemp* | *k10temp* | *tctl* | *cpu*)
|
||||
if [ -z "$cpu_zone" ] || [ "$t" -gt "$cpu_zone" ]; then cpu_zone=$t; fi
|
||||
;;
|
||||
esac
|
||||
if [ -z "$any_zone" ] || [ "$t" -gt "$any_zone" ]; then any_zone=$t; fi
|
||||
done
|
||||
|
||||
if [ -z "$cpu_zone" ]; then
|
||||
for d in /sys/class/hwmon/hwmon*/; do
|
||||
[ -r "${d}name" ] || continue
|
||||
case $(<"${d}name") in coretemp | k10temp | zenpower | cpu_thermal)
|
||||
for f in "${d}"temp*_input; do
|
||||
[ -r "$f" ] || continue
|
||||
t=$(<"$f")
|
||||
case $t in '' | *[!0-9]*) continue ;; esac
|
||||
if [ -z "$hw" ] || [ "$t" -gt "$hw" ]; then hw=$t; fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
t=${cpu_zone:-${hw:-$any_zone}}
|
||||
[ -n "$t" ] || exit 0
|
||||
printf '\U000f050f %d° ' $(((t + 500) / 1000))
|
||||
@@ -2,13 +2,21 @@
|
||||
# Without this, tmux collapses them to a plain Enter. Requires tmux 3.5+ for csi-u.
|
||||
set -g extended-keys on
|
||||
set -g extended-keys-format csi-u
|
||||
# termux reports xterm-256color: without RGB every #rrggbb below is approximated to the 256
|
||||
# palette on the phone, and without extkeys tmux never asks the terminal for Shift/Ctrl+Enter.
|
||||
# (kitty is xterm-kitty, whose terminfo already carries both.)
|
||||
set -as terminal-features 'xterm-256color:RGB:extkeys'
|
||||
# Let apps wrap escape sequences in DCS tmux passthrough (Claude Code / pi notifications,
|
||||
# Claude's progress bar) so they reach the outer terminal instead of being swallowed.
|
||||
set -g allow-passthrough on
|
||||
|
||||
# Mouse/touch reporting. Chiefly so the window list in the status bar is
|
||||
# tappable — tmux binds MouseDown1Status to `switch-client -t =` out of the box,
|
||||
# and status-format[1] below keeps the `range=window|…` markers that make each
|
||||
# entry a hit target, so a tap on a tab switches to it with no extra binding.
|
||||
# Wheel over the bar cycles windows; right-click a tab or the session name for
|
||||
# tmux's built-in menus (no touch equivalent — a phone can't right-click).
|
||||
# tmux's built-in menus; tmux-menus (plugin below) puts the same menus on prefix+\,
|
||||
# which a phone can tap through.
|
||||
#
|
||||
# The cost: drag-select now goes to tmux instead of kitty, so hold Shift when you
|
||||
# want kitty's own selection. Scroll wheel enters copy-mode on the pane rather
|
||||
@@ -21,6 +29,18 @@ set -g base-index 1
|
||||
setw -g pane-base-index 1
|
||||
set -g renumber-windows on
|
||||
|
||||
# One long agent turn scrolls past tmux's default 2000 lines; resurrect captures pane
|
||||
# contents from this buffer too. Applies to panes created after the change.
|
||||
set -g history-limit 50000
|
||||
|
||||
# Copy-mode: vi keys, v/y like an editor. set-clipboard on pushes every copy — tmux's own and
|
||||
# those apps inside make (nvim, pi) — over OSC 52 to the CLIENT terminal, so a yank on the
|
||||
# phone lands on the phone and on kitty lands on the desktop, wherever the server runs.
|
||||
setw -g mode-keys vi
|
||||
bind -T copy-mode-vi v send -X begin-selection
|
||||
bind -T copy-mode-vi y send -X copy-selection-and-cancel
|
||||
set -s set-clipboard on
|
||||
|
||||
# --- Session persistence (survives reboot) ---------------------------------
|
||||
# resurrect: save/restore sessions, windows, panes, layout and working dirs
|
||||
# (prefix + Ctrl-s to save, prefix + Ctrl-r to restore).
|
||||
@@ -29,6 +49,8 @@ set -g renumber-windows on
|
||||
set -g @plugin 'tmux-plugins/tpm'
|
||||
set -g @plugin 'tmux-plugins/tmux-resurrect'
|
||||
set -g @plugin 'tmux-plugins/tmux-continuum'
|
||||
# menus: tmux's context menus on prefix+\ (tappable; no right-click needed on a phone).
|
||||
set -g @plugin 'jaclu/tmux-menus'
|
||||
|
||||
# Also capture pane scrollback and restore running programs (nvim, …).
|
||||
# nvim 'session' strategy restores via Session.vim when present, else plain nvim.
|
||||
@@ -87,20 +109,23 @@ set -g status-left "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@left_ful
|
||||
|
||||
# Middle: flat window list — inactive muted, active mauve/bold, 2-space gaps.
|
||||
set -g window-status-separator " "
|
||||
setw -g window-status-format "#[fg=#7f849c]#I #[fg=#6c7086]#W"
|
||||
setw -g window-status-format "#[fg=#7f849c]#I#{P:#{E:@agent_badge}} #[fg=#6c7086]#W#[default]"
|
||||
setw -g window-status-current-format "#[fg=#cba6f7,bold]#I #[fg=#cdd6f4,bold]#W"
|
||||
setw -g window-status-activity-style "fg=#f9e2af"
|
||||
# The agent-state bell flags the window (monitor-bell) so the outer terminal buzzes; tmux's
|
||||
# default bell style is `reverse`, which painted the window entry — only the glyph gets colour.
|
||||
setw -g window-status-bell-style default
|
||||
|
||||
# Right: net dl/ul · cpu · mem │ date │ clock. continuum prepends its invisible
|
||||
# save hook to status-right (this block sits above the tpm `run` line, so the
|
||||
# hook survives every reload). Metrics are scripts/*.sh, symlinked into
|
||||
# ~/.config/tmux/scripts/. The hook lands ahead of the #{?} below, i.e. outside
|
||||
# it, so it keeps running for every client regardless of which variant is drawn.
|
||||
set -g @right_full "#[fg=#a6e3a1]#(~/.config/tmux/scripts/net.sh dl) #[fg=#f9e2af]#(~/.config/tmux/scripts/net.sh ul) #[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) #[fg=#fab387]#(~/.config/tmux/scripts/temp.sh)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M "
|
||||
# Right: net dl/ul · cpu · mem · temp │ date │ clock. One #() job per refresh: metrics.sh
|
||||
# prints the whole coloured segment (it was five separate scripts per tick). continuum
|
||||
# prepends its invisible save hook to status-right (this block sits above the tpm `run`
|
||||
# line, so the hook survives every reload). The hook lands ahead of the #{?} below, i.e.
|
||||
# outside it, so it keeps running for every client regardless of which variant is drawn.
|
||||
set -g @right_full "#(~/.config/tmux/scripts/metrics.sh full)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M "
|
||||
|
||||
# Compact: cpu + mem only. Net rates and the temp glyph go first (widest, least
|
||||
# useful on a phone), then the date, then the clock — a phone shows its own.
|
||||
set -g @right_compact "#[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) "
|
||||
set -g @right_compact "#(~/.config/tmux/scripts/metrics.sh compact) "
|
||||
|
||||
set -g status-right "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@right_full},#{E:@right_compact}}"
|
||||
|
||||
@@ -125,28 +150,48 @@ bind S {
|
||||
# Two rows: [0] drawn top-border rule · [1] the bar content. The rule doubles as
|
||||
# the separator from the pane content above; tmux status height is whole rows, so
|
||||
# one row above the content is the smallest gap that still shows a border.
|
||||
# Full-width run of ─ (yes|head|paste avoids %, backslashes and nested parens so
|
||||
# tmux's strftime/quote/#() parsing leave it alone); tmux truncates it to width.
|
||||
# The rule is a literal run of ─ in @rule (300 columns; tmux clips it to the client
|
||||
# width), so drawing it costs no process.
|
||||
# status-format[1] is tmux 3.7's default content template (status-left/windows/right).
|
||||
set -g status 2
|
||||
set -g status-format[0] "#[fg=#6c7086,bg=default]#(yes ─ | head -n 400 | paste -sd '')"
|
||||
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
|
||||
set -g @rule "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
set -g status-format[0] "#[fg=#6c7086,bg=default]#{@rule}"
|
||||
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[range=user|agentnext] #[fg=#6c7086]»#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
|
||||
|
||||
# The `+` appended to the window list above sits in a `range=user|newwin`
|
||||
# marker. Per tmux(1), a user range triggers the same `Status` mouse key as a
|
||||
# window range and exposes its argument in #{mouse_status_range} — window ranges
|
||||
# report the literal string "window" — so one binding can serve both: click a
|
||||
# tab to select it (tmux's stock behaviour, kept in the else branch), click the
|
||||
# `+` to open a new window. Padded with spaces on both sides so it's a wide
|
||||
# enough target to hit with a thumb.
|
||||
# `+` to open a new window, click the `»` to jump to the next window with an agent
|
||||
# mark (scripts/agent-next.sh, also prefix+Tab). Padded with spaces on both sides so
|
||||
# each is a wide enough target to hit with a thumb.
|
||||
bind -n MouseDown1Status {
|
||||
if -F '#{==:#{mouse_status_range},newwin}' {
|
||||
new-window
|
||||
} {
|
||||
switch-client -t =
|
||||
if -F '#{==:#{mouse_status_range},agentnext}' {
|
||||
run-shell '~/.config/tmux/scripts/agent-next.sh'
|
||||
} {
|
||||
switch-client -t =
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Agent state per pane — set by ~/.config/tmux/scripts/agent-state.sh <tool> <state> from the
|
||||
# Claude Code / codex / pi / opencode hooks: @agent = busy|done|input|error. @agent_badge draws
|
||||
# one glyph glued to the window number ("2✓ name") in the window list, the same for every tool: ✓ done (green),
|
||||
# ? waiting for input (yellow), ! error (red); busy draws nothing. Visiting the pane only takes
|
||||
# the colour away (pane-focus-in sets @agent_seen; needs focus-events): the glyph stays, grey,
|
||||
# until the next prompt turns the state busy. prefix K clears every mark; prefix Tab (or the »
|
||||
# in the bar) jumps to the next window with a mark, coloured ones first. A coloured glyph
|
||||
# always means "this happened while you were not looking".
|
||||
set -g @agent_badge "#{?#{==:#{@agent},done},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#a6e3a1]}✓,#{?#{==:#{@agent},input},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#f9e2af]}?,#{?#{==:#{@agent},error},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#f38ba8]}!,}}}"
|
||||
set -g focus-events on
|
||||
set-hook -g pane-focus-in 'set -p @agent_seen 1'
|
||||
bind Tab run-shell '~/.config/tmux/scripts/agent-next.sh'
|
||||
bind K run-shell 'for p in $(tmux list-panes -a -F "##{pane_id}"); do tmux set -p -t $p -u @agent; tmux set -p -t $p -u @agent_seen; done' \; display-message "agent marks cleared"
|
||||
|
||||
# Round out the theme so no default green leaks into borders/messages/modes.
|
||||
set -g pane-border-style "fg=#313244"
|
||||
set -g pane-active-border-style "fg=#cba6f7"
|
||||
|
||||
Reference in new issue
Block a user