Skip to content

Commit dceaf89

Browse files
committed
Script to marquee scroll buffer titles
Update title_scroll.py
1 parent 7be965a commit dceaf89

1 file changed

Lines changed: 288 additions & 0 deletions

File tree

‎python/title_scroll.py‎

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
"""Marquee-style auto-scrolling for long buffer titles."""
2+
3+
SCRIPT_NAME = "title_scroll"
4+
SCRIPT_AUTHOR = "nleytem"
5+
SCRIPT_EMAIL = "nmleytem@gmail.com"
6+
SCRIPT_VERSION = "1.0.0"
7+
SCRIPT_LICENSE = "GPL-3.0-only"
8+
SCRIPT_DESC = (
9+
"Auto-scrolls buffer titles that don't fit the window width, "
10+
"marquee-style, with a marker between the end and repeated start "
11+
"(note: embedded title colors are stripped while scrolling)"
12+
)
13+
COPYRIGHT = "SPDX-FileCopyrightText: 2026 Nick Leytem <nmleytem@gmail.com>"
14+
LICENSE = "SPDX-License-Identifier: GPL-3.0-only"
15+
16+
try:
17+
import weechat # type: ignore[import]
18+
except ImportError:
19+
print("This script must be run under WeeChat.")
20+
print("Get WeeChat now at: https://weechat.org/")
21+
IMPORT_OK = False
22+
23+
SETTINGS = {
24+
"enabled": (
25+
"on",
26+
"enable/disable scrolling (off: item shows static/truncated text)",
27+
),
28+
"interval": (
29+
"200",
30+
"milliseconds between each scroll step (lower = faster)",
31+
),
32+
"step": (
33+
"1",
34+
"characters to advance per scroll step (higher = faster)",
35+
),
36+
"pause": (
37+
"2",
38+
"scroll steps to pause when a title's loop restarts (0: no pause)",
39+
),
40+
"separator": (
41+
" • ",
42+
"text inserted between the end and the repeated start of a "
43+
"scrolled title",
44+
),
45+
"width_margin": (
46+
"2",
47+
"characters subtracted from the window width when deciding "
48+
"if/how much to scroll",
49+
),
50+
"text_color": (
51+
"",
52+
"WeeChat color name for the title text (empty: bar's default "
53+
"foreground color)",
54+
),
55+
"separator_color": (
56+
"cyan",
57+
"WeeChat color name for the separator marker (empty: same "
58+
"color as the title text)",
59+
),
60+
}
61+
62+
# buffer pointer -> {"offset": int, "pause_left": int}
63+
SCROLL_STATE = {}
64+
65+
TIMER_HOOK = None
66+
67+
68+
def get_str(name):
69+
return weechat.config_get_plugin(name)
70+
71+
72+
def get_int(name):
73+
try:
74+
return int(weechat.config_get_plugin(name))
75+
except (TypeError, ValueError):
76+
return 0
77+
78+
79+
def is_enabled():
80+
return get_str("enabled") == "on"
81+
82+
83+
def title_scroll_build_cb(data, item, window):
84+
if not window:
85+
window = weechat.current_window()
86+
buffer = weechat.window_get_pointer(window, "buffer")
87+
if not buffer:
88+
buffer = weechat.current_buffer()
89+
90+
title = weechat.buffer_get_string(buffer, "title")
91+
plain = weechat.string_remove_color(title, "")
92+
if not plain:
93+
return ""
94+
95+
margin = get_int("width_margin")
96+
width = weechat.window_get_integer(window, "win_width") - margin
97+
if width < 1:
98+
width = 1
99+
100+
text_color_name = get_str("text_color")
101+
color_text = weechat.color(text_color_name) if text_color_name else weechat.color("bar_fg")
102+
103+
if len(plain) <= width:
104+
return "%s%s" % (color_text, plain)
105+
106+
separator = get_str("separator")
107+
sep_color_name = get_str("separator_color")
108+
color_sep = weechat.color(sep_color_name) if sep_color_name else color_text
109+
110+
cycle_len = len(plain) + len(separator)
111+
state = SCROLL_STATE.get(buffer, {"offset": 0, "pause_left": 0})
112+
offset = state.get("offset", 0) % cycle_len if cycle_len else 0
113+
114+
looped = plain + separator + plain
115+
src = ("T" * len(plain)) + ("S" * len(separator)) + ("T" * len(plain))
116+
chunk = looped[offset:offset + width]
117+
chunk_src = src[offset:offset + width]
118+
119+
out = []
120+
current = None
121+
for ch, s in zip(chunk, chunk_src):
122+
col = color_sep if s == "S" else color_text
123+
if col != current:
124+
out.append(col)
125+
current = col
126+
out.append(ch)
127+
return "".join(out)
128+
129+
130+
def title_scroll_timer_cb(data, remaining_calls):
131+
if not is_enabled():
132+
return weechat.WEECHAT_RC_OK
133+
134+
step = max(get_int("step"), 1)
135+
pause_ticks = get_int("pause")
136+
margin = get_int("width_margin")
137+
separator = get_str("separator")
138+
139+
active_buffers = set()
140+
any_scrolling = False
141+
142+
infolist = weechat.infolist_get("window", "", "")
143+
if infolist:
144+
while weechat.infolist_next(infolist):
145+
buffer = weechat.infolist_pointer(infolist, "buffer")
146+
if not buffer:
147+
continue
148+
win_width = weechat.infolist_integer(infolist, "win_width") - margin
149+
if win_width < 1:
150+
win_width = 1
151+
152+
title = weechat.buffer_get_string(buffer, "title")
153+
plain = weechat.string_remove_color(title, "")
154+
active_buffers.add(buffer)
155+
156+
if len(plain) <= win_width:
157+
continue
158+
159+
any_scrolling = True
160+
cycle_len = len(plain) + len(separator)
161+
state = SCROLL_STATE.setdefault(buffer, {"offset": 0, "pause_left": 0})
162+
if state["pause_left"] > 0:
163+
state["pause_left"] -= 1
164+
else:
165+
new_offset = state["offset"] + step
166+
if cycle_len and new_offset >= cycle_len:
167+
new_offset %= cycle_len
168+
if pause_ticks:
169+
state["pause_left"] = pause_ticks
170+
state["offset"] = new_offset
171+
weechat.infolist_free(infolist)
172+
173+
for buf in list(SCROLL_STATE.keys()):
174+
if buf not in active_buffers:
175+
del SCROLL_STATE[buf]
176+
177+
if any_scrolling:
178+
weechat.bar_item_update(BAR_ITEM_NAME)
179+
180+
return weechat.WEECHAT_RC_OK
181+
182+
183+
def title_scroll_title_changed_cb(data, signal, signal_data):
184+
buffer = signal_data
185+
if buffer in SCROLL_STATE:
186+
SCROLL_STATE[buffer] = {"offset": 0, "pause_left": 0}
187+
weechat.bar_item_update(BAR_ITEM_NAME)
188+
return weechat.WEECHAT_RC_OK
189+
190+
191+
def title_scroll_setup_timer():
192+
global TIMER_HOOK
193+
if TIMER_HOOK:
194+
weechat.unhook(TIMER_HOOK)
195+
TIMER_HOOK = None
196+
if is_enabled():
197+
interval = max(get_int("interval"), 10)
198+
TIMER_HOOK = weechat.hook_timer(interval, 0, 0, "title_scroll_timer_cb", "")
199+
200+
201+
def title_scroll_config_cb(data, option, value):
202+
if option.endswith(".enabled") or option.endswith(".interval"):
203+
title_scroll_setup_timer()
204+
return weechat.WEECHAT_RC_OK
205+
206+
207+
def title_scroll_cmd_cb(data, buffer, args):
208+
args = args.strip()
209+
if args == "enable":
210+
weechat.config_set_plugin("enabled", "on")
211+
weechat.prnt("", "%s: enabled" % SCRIPT_NAME)
212+
elif args == "disable":
213+
weechat.config_set_plugin("enabled", "off")
214+
weechat.prnt("", "%s: disabled" % SCRIPT_NAME)
215+
elif args == "reset":
216+
SCROLL_STATE.clear()
217+
weechat.bar_item_update(BAR_ITEM_NAME)
218+
weechat.prnt("", "%s: scroll positions reset" % SCRIPT_NAME)
219+
else:
220+
status = "enabled" if is_enabled() else "disabled"
221+
weechat.prnt(
222+
"",
223+
"%s: %s (interval=%sms step=%s pause=%s)"
224+
% (SCRIPT_NAME, status, get_int("interval"), get_int("step"), get_int("pause")),
225+
)
226+
return weechat.WEECHAT_RC_OK
227+
228+
229+
def title_scroll_unload_script():
230+
global TIMER_HOOK
231+
if TIMER_HOOK:
232+
weechat.unhook(TIMER_HOOK)
233+
TIMER_HOOK = None
234+
return weechat.WEECHAT_RC_OK
235+
236+
237+
def title_scroll_main():
238+
if not weechat.register(
239+
SCRIPT_NAME,
240+
SCRIPT_AUTHOR,
241+
SCRIPT_VERSION,
242+
SCRIPT_LICENSE,
243+
SCRIPT_DESC,
244+
"title_scroll_unload_script",
245+
"",
246+
):
247+
return
248+
249+
version = weechat.info_get("version_number", "") or 0
250+
for option, value in SETTINGS.items():
251+
if not weechat.config_is_set_plugin(option):
252+
weechat.config_set_plugin(option, value[0])
253+
if int(version) >= 0x00030500:
254+
weechat.config_set_desc_plugin(
255+
option, '%s (default: "%s")' % (value[1], value[0])
256+
)
257+
258+
weechat.bar_item_new(BAR_ITEM_NAME, "title_scroll_build_cb", "")
259+
weechat.hook_signal("buffer_title_changed", "title_scroll_title_changed_cb", "")
260+
weechat.hook_config(
261+
"plugins.var.python.%s.*" % SCRIPT_NAME, "title_scroll_config_cb", ""
262+
)
263+
weechat.hook_command(
264+
"title_scroll",
265+
"Control the scrolling title bar item",
266+
"enable|disable|status|reset",
267+
" enable: turn scrolling on\n"
268+
" disable: turn scrolling off (item still shows static/truncated text)\n"
269+
" status: show current settings\n"
270+
" reset: reset all scroll positions to the start\n"
271+
"\n"
272+
"Add the item to your title bar with:\n"
273+
' /set weechat.bar.title.items "%s"' % BAR_ITEM_NAME,
274+
"enable|disable|status|reset",
275+
"title_scroll_cmd_cb",
276+
"",
277+
)
278+
279+
title_scroll_setup_timer()
280+
weechat.prnt(
281+
"",
282+
'%s: add the scrolling title item to your bar with: '
283+
'/set weechat.bar.title.items "%s"' % (SCRIPT_NAME, BAR_ITEM_NAME),
284+
)
285+
286+
287+
if __name__ == "__main__" and IMPORT_OK:
288+
title_scroll_main()

0 commit comments

Comments
 (0)