-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for .clinktheme and .clinkprompt files.
Refer to the documentation for details on how they work. *.clinktheme files can contain color theme definitions for Clink. *.clinkprompt files contain custom prompt scripts for Clink. You can easily switch between different color themes and custom prompts, and they can be easily shared with other users. - Added `clink config theme` commands for color themes. - Added `clink config prompt` commands for custom prompts. - Added some color themes to be included with Clink. - Added some custom prompts to be included with Clink. - Added `%CLINK_COLORTHEME%` and `%CLINK_CUSTOMPROMPT%` to override color settings and custom prompt only in the current session, allowing multiple concurrent Clink sessions to use different color themes and different custom prompts. - Added `italic`, `reverse`, `#XXXXXX`, and `#XXX` for color settings. - Added `settings.parsecolor()` and `settings.formatcolor()`. - Added theme APIs; `clink.getthemes()`, `clink.readtheme()`, and `clink.applytheme()`. - Added `os.createguid()`. - Added optional `cookie` param to `clink.promptcoroutine()`. Such a simple change with such significant benefits. Why oh why didn't I do this long ago. - Fixed `rl.collapsetilde()` which didn't do its job. - Fixed `settings.set()` which didn't save the setting properly. - Fixed color of leading blank lines in the prompt. They should use the default color, rather than the `color.prompt` color. The problem was obvious when a background color is specified. - Fixed leftover text after OS word wrap upon resizing terminal. E.g. have a 2-line prompt whose first line extends the full width of the screen, and then increase the point size of the terminal font. Part of the wrapped text stayed visible after refiltering the prompt. - Fixed comment row to stop drawing during clink-select-complete. - Fixed %clink_dummy_capture_env% to stop leaking into the environment.
- Loading branch information
1 parent
97d2e1e
commit 8bc5914
Showing
63 changed files
with
37,995 additions
and
21,715 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,350 @@ | ||
-- Copyright (c) 2024 Christopher Antos | ||
-- License: http://opensource.org/licenses/MIT | ||
|
||
-- luacheck: globals git | ||
|
||
-------------------------------------------------------------------------------- | ||
local loaded_clinkprompts = {} | ||
local clinkprompt_wrapping_module | ||
|
||
local function clinkprompt_loader(module) | ||
local lower_module = clink.lower(module) | ||
local ret = loaded_clinkprompts[lower_module] | ||
if not ret then | ||
local func, loaderr = loadfile(module) | ||
if func then | ||
local old = clinkprompt_wrapping_module | ||
clinkprompt_wrapping_module = lower_module | ||
|
||
local ok, funcerr = pcall(function() ret = func() end) | ||
|
||
clinkprompt_wrapping_module = old | ||
ret = ok and (ret or true) or funcerr or false | ||
else | ||
ret = loaderr or false | ||
end | ||
loaded_clinkprompts[lower_module] = ret | ||
end | ||
return ret | ||
end | ||
|
||
local function clinkprompt_searcher(module) | ||
if clink.lower(path.getextension(module)) == ".clinkprompt" and os.isfile(module) then | ||
return clinkprompt_loader | ||
end | ||
end | ||
|
||
table.insert(package.searchers, 2, clinkprompt_searcher) | ||
|
||
-------------------------------------------------------------------------------- | ||
local function add_dirs_from_var(t, var, subdir) | ||
if var and var ~= "" then | ||
local dirs = string.explode(var, ";", '"') | ||
for _,d in ipairs(dirs) do | ||
d = d:gsub("^%s+", ""):gsub("%s+$", "") | ||
d = rl.expandtilde(d:gsub('"', "")) | ||
if subdir then | ||
d = path.join(d, "themes") | ||
end | ||
d = path.getdirectory(path.join(d, "")) -- Makes sure no trailing path separator. | ||
local key = clink.lower(d) | ||
if not t[key] then | ||
t[key] = true | ||
table.insert(t, d) | ||
end | ||
end | ||
return true | ||
end | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
local function get_theme_files(name, ext) | ||
if name then | ||
if os.isfile(name) then | ||
if clink.lower(path.getextension(name) or "") == ext then | ||
return os.getfullpathname(name) | ||
end | ||
elseif os.isfile(name..ext) then | ||
return os.getfullpathname(name..ext) | ||
end | ||
end | ||
|
||
name = name and clink.lower(name) or nil | ||
|
||
-- Order matters: if the same name exists in two directories, the first | ||
-- one in the scripts path order wins. | ||
local dirs = {} | ||
local env = os.getenv("CLINK_THEMES_DIR") | ||
add_dirs_from_var(dirs, env, false) | ||
add_dirs_from_var(dirs, clink._get_scripts_path(), true) | ||
|
||
local list = {} | ||
local indexed = {} | ||
for _,dir in ipairs(dirs) do | ||
local t = os.globfiles(path.join(dir, "*"..ext), true) | ||
if t then | ||
for _,entry in ipairs(t) do | ||
if entry.type:find("file") then | ||
local basename = path.getbasename(entry.name) | ||
if basename then | ||
local fullname = path.join(dir, entry.name) | ||
local lowername = clink.lower(basename) | ||
if not indexed[lowername] then | ||
indexed[lowername] = fullname | ||
table.insert(list, basename) | ||
if name and name == lowername or name == clink.lower(fullname) then | ||
return fullname -- Found. | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
if name then | ||
return -- Not found. | ||
end | ||
|
||
table.sort(list, string.comparematches) | ||
return list, indexed -- Separate tables in case a theme is named "1"..ext, for example. | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
--- -name: clink.getprompts | ||
--- -ver: 1.7.0 | ||
--- -arg: [name:string] | ||
--- -ret: (see remarks below) | ||
--- This has two different behaviors, depending on whether the optional | ||
--- <span class="arg">name</span> argument is provided: | ||
--- | ||
--- If <span class="arg">name</span> is omitted, this returns two tables. | ||
--- The first table is an array of custom prompt names. The second table is a | ||
--- map of lowercase names to their corresponding file paths. | ||
--- -show: local names, map = clink.getprompts() | ||
--- -show: for _, name in ipairs(names) do | ||
--- -show: local file = map[clink.lower(name)] | ||
--- -show: print(string.format('"%s" -> "%s"', name, file)) | ||
--- -show: end | ||
--- | ||
--- If <span class="arg">name</span> is a string, this returns the file path | ||
--- of a matching custom prompt file. If the ".clinkprompt" extension is not | ||
--- present, it is assumed. If no matching prompt file is found, nil is | ||
--- returned. | ||
--- -show: clink.getprompt("My Prompt Name") | ||
--- -show: -- Could return "My Prompt Name.clinkprompt" if that file exists in the current directory. | ||
--- -show: -- Could return "c:\myclink\scripts\themes\My Prompt Name.clinkprompt" if that exists. | ||
--- -show: -- Etc. | ||
--- | ||
--- Refer to <a href="#customisingtheprompt">Customizing the Prompt</a> for | ||
--- more information. | ||
function clink.getprompts(name) | ||
return get_theme_files(name, ".clinkprompt") | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
--- -name: clink.getthemes | ||
--- -ver: 1.7.0 | ||
--- -arg: [name:string] | ||
--- -ret: (see remarks below) | ||
--- This has two different behaviors, depending on whether the optional | ||
--- <span class="arg">name</span> argument is provided: | ||
--- | ||
--- If <span class="arg">name</span> is omitted, this returns two tables. | ||
--- The first table is an array of theme names. The second table is a map of | ||
--- lowercase names to their corresponding file paths. | ||
--- -show: local names, map = clink.getthemes() | ||
--- -show: for _, name in ipairs(names) do | ||
--- -show: local file = map[clink.lower(name)] | ||
--- -show: print(string.format('"%s" -> "%s"', name, file)) | ||
--- -show: end | ||
--- | ||
--- If <span class="arg">name</span> is a string, this returns the file path | ||
--- of a matching theme file. If the ".clinktheme" extension is not present, | ||
--- it is assumed. If no matching theme file is found, nil is returned. | ||
--- -show: clink.getthemes("My Theme Name") | ||
--- -show: -- Could return "My Theme Name.clinktheme" if that file exists in the current directory. | ||
--- -show: -- Could return "c:\myclink\scripts\themes\My Theme Name.clinktheme" if that exists. | ||
--- -show: -- Etc. | ||
--- | ||
--- Refer to <a href="#color-themes">Color Themes</a> for more information. | ||
function clink.getthemes(name) | ||
return get_theme_files(name, ".clinktheme") | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
--- -name: clink.readtheme | ||
--- -ver: 1.7.0 | ||
--- -arg: theme:string | ||
--- -ret: table, string | ||
--- Reads the specified theme and returns a table with its settings. If | ||
--- unsuccessful, it returns nil followed by a message describing the failure. | ||
--- | ||
--- <span class="arg">theme</span> can be a filename or the title of an | ||
--- installed theme. | ||
--- | ||
--- Refer to <a href="#color-themes">Color Themes</a> for more information. | ||
function clink.readtheme(theme) | ||
local fullname = clink.getthemes(theme) | ||
if not fullname then | ||
return nil, "Theme '"..theme.."' not found." | ||
end | ||
theme = fullname | ||
|
||
local ini = settings._parseini(theme) | ||
if not ini then | ||
return nil, "Error reading '"..theme.."'." | ||
end | ||
|
||
local message | ||
for _, t in ipairs(ini) do | ||
if t.name == "match.coloring_rules" then | ||
if os.getenv("CLINK_MATCH_COLORS") then | ||
message = "CLINK_MATCH_COLORS overrides the theme's match.coloring_rules." | ||
end | ||
elseif not t.name:find("^color.") then | ||
return nil, "Unexpected setting name '"..t.name.."' in '"..theme.."'." | ||
end | ||
ini[t.name] = settings.parsecolor(t.value) -- Allow indexed lookup. | ||
end | ||
|
||
return ini, message | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
--- -name: clink.applytheme | ||
--- -ver: 1.7.0 | ||
--- -arg: theme:string | ||
--- -arg: [clearall:boolean] | ||
--- -arg: [no_save:boolean] | ||
--- -ret: table, message | ||
--- Finds the <span class="arg">theme</span> and tries to apply its setting | ||
--- (which are saved in the current profile settings, which affects all Clink | ||
--- sessions using that profile directory). | ||
--- | ||
--- If successful, it returns true. If unsuccessful, it returns nil followed | ||
--- by a message describing the failure. | ||
--- | ||
--- Applying a color theme clears the built-in color settings to their default | ||
--- values before loading the theme. If the optional | ||
--- <span class="arg">clearall</span> argument is true, then it also clears | ||
--- any color settings added by Lua scripts. | ||
--- | ||
--- If the optional <span class="arg">no_save</span> argument is true, then | ||
--- the color theme is loaded into memory but is not saved back to the Clink | ||
--- settings file. | ||
--- | ||
--- Refer to <a href="#color-themes">Color Themes</a> for more information. | ||
function clink.applytheme(file, clearall, no_save) | ||
local ini, message = clink.readtheme(file) | ||
if not ini then | ||
return nil, message | ||
end | ||
|
||
-- FUTURE: what about match coloring rules? | ||
clink._add_clear_colors(ini, clearall, false) | ||
settings._overlay(ini, no_save and true or nil) | ||
return ini | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
function clink._load_colortheme_in_memory(theme) | ||
if type(theme) == "string" then | ||
theme = theme:gsub('"', ''):gsub('%s+$', '') | ||
if theme == "" then | ||
return | ||
end | ||
theme = clink.readtheme(theme) | ||
end | ||
if type(theme) == "table" then | ||
clink._add_clear_colors(theme) | ||
settings._overlay(theme, true--[[in_memory_only]]) | ||
end | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
function clink._add_clear_colors(ini, all, rules) | ||
local has = {} | ||
for _,t in ipairs(ini) do | ||
if t.name then | ||
has[t.name] = true | ||
end | ||
end | ||
for _,e in ipairs(settings.list()) do | ||
if not has[e.match] then | ||
if e.match:find("^color%.") then | ||
if all or not e.source then | ||
table.insert(ini, {name=e.match}) | ||
end | ||
elseif rules and e.match == "match.coloring_rules" then | ||
table.insert(ini, {name=e.match}) | ||
end | ||
end | ||
end | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
function clink._clear_colors(all, rules) | ||
local ini = {} | ||
clink._add_clear_colors(ini, all, rules) | ||
settings._overlay(ini) | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
function clink._show_prompt_demo(module) | ||
git._fake = { | ||
status = { | ||
branch = "main", | ||
HEAD = "a1b2c3d", | ||
upstream = "origin", | ||
stashes = 27, | ||
dirty = true, | ||
behind = 19, | ||
working = { | ||
modify = 3, | ||
untracked = 1, | ||
}, | ||
total = { | ||
modify = 3, | ||
}, | ||
tracked = 3, | ||
untracked = 1, | ||
}, | ||
} | ||
|
||
clink.print("\x1b[m", NONL) | ||
|
||
local m = clink._activate_clinkprompt_module(module) | ||
|
||
if type(m) == "string" then | ||
print(m) | ||
elseif type(m) ~= "table" or not m.demo then | ||
local simulated_cursor = "\x1b[0;7m \x1b[m" | ||
local left = clink._expand_prompt_codes(os.getenv("PROMPT") or "$p$g") | ||
local right = clink._expand_prompt_codes(os.getenv("CLINK_RPROMPT") or "", true) | ||
left, right = clink._filter_prompt(left, right, "", 1) | ||
left = (left or "")..simulated_cursor | ||
right = right or "" | ||
local left_width = console.cellcount(left:gsub("^.*\n", "")) | ||
local right_width = console.cellcount(right) | ||
if right_width <= 0 or left_width + right_width + 4 > console.getwidth() then | ||
right = "" | ||
else | ||
right = string.rep(" ", console.getwidth() - left_width - right_width)..right | ||
end | ||
clink.print(left..right) | ||
else | ||
m.demo() | ||
end | ||
|
||
clink._activate_clinkprompt_module(nil) | ||
|
||
clink.print("\x1b[m\x1b[K", NONL) | ||
|
||
git._fake = nil | ||
end | ||
|
||
-------------------------------------------------------------------------------- | ||
function clink._get_clinkprompt_wrapping_module() | ||
return clinkprompt_wrapping_module | ||
end |
Oops, something went wrong.