Skip to content

Instantly share code, notes, and snippets.

@tyru
Created September 22, 2018 17:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tyru/cf1236e90c403d17399dd576d7496237 to your computer and use it in GitHub Desktop.
Save tyru/cf1236e90c403d17399dd576d7496237 to your computer and use it in GitHub Desktop.
scriptencoding utf-8
let s:save_cpo = &cpo
set cpo&vim
" index = xterm number
" cf.
" * :help cterm-colors
" * https://jonasjacek.github.io/colors/
let s:color_table = [
\ {'name': 'Black', 'code': 0x000000},
\ {'name': 'DarkRed', 'code': 0x800000},
\ {'name': 'DarkGreen', 'code': 0x008000},
\ {'name': 'Brown', 'code': 0x808000},
\ {'name': 'DarkBlue', 'code': 0x000080},
\ {'name': 'DarkMagenta', 'code': 0x800080},
\ {'name': 'DarkCyan', 'code': 0x008080},
\ {'name': 'Grey', 'code': 0xc0c0c0},
\ {'name': 'DarkGrey', 'code': 0x808080},
\ {'name': 'Red', 'code': 0xff0000},
\ {'name': 'Green', 'code': 0x00ff00},
\ {'name': 'Yellow', 'code': 0xffff00},
\ {'name': 'Blue', 'code': 0x0000ff},
\ {'name': 'Magenta', 'code': 0xff00ff},
\ {'name': 'Cyan', 'code': 0x00ffff},
\ {'name': 'White', 'code': 0xffffff},
\
\ {'name': 'Grey0', 'code': 0x000000},
\ {'name': 'NavyBlue', 'code': 0x00005f},
\ {'name': 'DarkBlue', 'code': 0x000087},
\ {'name': 'Blue3', 'code': 0x0000af},
\ {'name': 'Blue3', 'code': 0x0000d7},
\ {'name': 'Blue1', 'code': 0x0000ff},
\ {'name': 'DarkGreen', 'code': 0x005f00},
\ {'name': 'DeepSkyBlue4', 'code': 0x005f5f},
\ {'name': 'DeepSkyBlue4', 'code': 0x005f87},
\ {'name': 'DeepSkyBlue4', 'code': 0x005faf},
\ {'name': 'DodgerBlue3', 'code': 0x005fd7},
\ {'name': 'DodgerBlue2', 'code': 0x005fff},
\ {'name': 'Green4', 'code': 0x008700},
\ {'name': 'SpringGreen4', 'code': 0x00875f},
\ {'name': 'Turquoise4', 'code': 0x008787},
\ {'name': 'DeepSkyBlue3', 'code': 0x0087af},
\ {'name': 'DeepSkyBlue3', 'code': 0x0087d7},
\ {'name': 'DodgerBlue1', 'code': 0x0087ff},
\ {'name': 'Green3', 'code': 0x00af00},
\ {'name': 'SpringGreen3', 'code': 0x00af5f},
\ {'name': 'DarkCyan', 'code': 0x00af87},
\ {'name': 'LightSeaGreen', 'code': 0x00afaf},
\ {'name': 'DeepSkyBlue2', 'code': 0x00afd7},
\ {'name': 'DeepSkyBlue1', 'code': 0x00afff},
\ {'name': 'Green3', 'code': 0x00d700},
\ {'name': 'SpringGreen3', 'code': 0x00d75f},
\ {'name': 'SpringGreen2', 'code': 0x00d787},
\ {'name': 'Cyan3', 'code': 0x00d7af},
\ {'name': 'DarkTurquoise', 'code': 0x00d7d7},
\ {'name': 'Turquoise2', 'code': 0x00d7ff},
\ {'name': 'Green1', 'code': 0x00ff00},
\ {'name': 'SpringGreen2', 'code': 0x00ff5f},
\ {'name': 'SpringGreen1', 'code': 0x00ff87},
\ {'name': 'MediumSpringGreen', 'code': 0x00ffaf},
\ {'name': 'Cyan2', 'code': 0x00ffd7},
\ {'name': 'Cyan1', 'code': 0x00ffff},
\ {'name': 'DarkRed', 'code': 0x5f0000},
\ {'name': 'DeepPink4', 'code': 0x5f005f},
\ {'name': 'Purple4', 'code': 0x5f0087},
\ {'name': 'Purple4', 'code': 0x5f00af},
\ {'name': 'Purple3', 'code': 0x5f00d7},
\ {'name': 'BlueViolet', 'code': 0x5f00ff},
\ {'name': 'Orange4', 'code': 0x5f5f00},
\ {'name': 'Grey37', 'code': 0x5f5f5f},
\ {'name': 'MediumPurple4', 'code': 0x5f5f87},
\ {'name': 'SlateBlue3', 'code': 0x5f5faf},
\ {'name': 'SlateBlue3', 'code': 0x5f5fd7},
\ {'name': 'RoyalBlue1', 'code': 0x5f5fff},
\ {'name': 'Chartreuse4', 'code': 0x5f8700},
\ {'name': 'DarkSeaGreen4', 'code': 0x5f875f},
\ {'name': 'PaleTurquoise4', 'code': 0x5f8787},
\ {'name': 'SteelBlue', 'code': 0x5f87af},
\ {'name': 'SteelBlue3', 'code': 0x5f87d7},
\ {'name': 'CornflowerBlue', 'code': 0x5f87ff},
\ {'name': 'Chartreuse3', 'code': 0x5faf00},
\ {'name': 'DarkSeaGreen4', 'code': 0x5faf5f},
\ {'name': 'CadetBlue', 'code': 0x5faf87},
\ {'name': 'CadetBlue', 'code': 0x5fafaf},
\ {'name': 'SkyBlue3', 'code': 0x5fafd7},
\ {'name': 'SteelBlue1', 'code': 0x5fafff},
\ {'name': 'Chartreuse3', 'code': 0x5fd700},
\ {'name': 'PaleGreen3', 'code': 0x5fd75f},
\ {'name': 'SeaGreen3', 'code': 0x5fd787},
\ {'name': 'Aquamarine3', 'code': 0x5fd7af},
\ {'name': 'MediumTurquoise', 'code': 0x5fd7d7},
\ {'name': 'SteelBlue1', 'code': 0x5fd7ff},
\ {'name': 'Chartreuse2', 'code': 0x5fff00},
\ {'name': 'SeaGreen2', 'code': 0x5fff5f},
\ {'name': 'SeaGreen1', 'code': 0x5fff87},
\ {'name': 'SeaGreen1', 'code': 0x5fffaf},
\ {'name': 'Aquamarine1', 'code': 0x5fffd7},
\ {'name': 'DarkSlateGray2', 'code': 0x5fffff},
\ {'name': 'DarkRed', 'code': 0x870000},
\ {'name': 'DeepPink4', 'code': 0x87005f},
\ {'name': 'DarkMagenta', 'code': 0x870087},
\ {'name': 'DarkMagenta', 'code': 0x8700af},
\ {'name': 'DarkViolet', 'code': 0x8700d7},
\ {'name': 'Purple', 'code': 0x8700ff},
\ {'name': 'Orange4', 'code': 0x875f00},
\ {'name': 'LightPink4', 'code': 0x875f5f},
\ {'name': 'Plum4', 'code': 0x875f87},
\ {'name': 'MediumPurple3', 'code': 0x875faf},
\ {'name': 'MediumPurple3', 'code': 0x875fd7},
\ {'name': 'SlateBlue1', 'code': 0x875fff},
\ {'name': 'Yellow4', 'code': 0x878700},
\ {'name': 'Wheat4', 'code': 0x87875f},
\ {'name': 'Grey53', 'code': 0x878787},
\ {'name': 'LightSlateGrey', 'code': 0x8787af},
\ {'name': 'MediumPurple', 'code': 0x8787d7},
\ {'name': 'LightSlateBlue', 'code': 0x8787ff},
\ {'name': 'Yellow4', 'code': 0x87af00},
\ {'name': 'DarkOliveGreen3', 'code': 0x87af5f},
\ {'name': 'DarkSeaGreen', 'code': 0x87af87},
\ {'name': 'LightSkyBlue3', 'code': 0x87afaf},
\ {'name': 'LightSkyBlue3', 'code': 0x87afd7},
\ {'name': 'SkyBlue2', 'code': 0x87afff},
\ {'name': 'Chartreuse2', 'code': 0x87d700},
\ {'name': 'DarkOliveGreen3', 'code': 0x87d75f},
\ {'name': 'PaleGreen3', 'code': 0x87d787},
\ {'name': 'DarkSeaGreen3', 'code': 0x87d7af},
\ {'name': 'DarkSlateGray3', 'code': 0x87d7d7},
\ {'name': 'SkyBlue1', 'code': 0x87d7ff},
\ {'name': 'Chartreuse1', 'code': 0x87ff00},
\ {'name': 'LightGreen', 'code': 0x87ff5f},
\ {'name': 'LightGreen', 'code': 0x87ff87},
\ {'name': 'PaleGreen1', 'code': 0x87ffaf},
\ {'name': 'Aquamarine1', 'code': 0x87ffd7},
\ {'name': 'DarkSlateGray1', 'code': 0x87ffff},
\ {'name': 'Red3', 'code': 0xaf0000},
\ {'name': 'DeepPink4', 'code': 0xaf005f},
\ {'name': 'MediumVioletRed', 'code': 0xaf0087},
\ {'name': 'Magenta3', 'code': 0xaf00af},
\ {'name': 'DarkViolet', 'code': 0xaf00d7},
\ {'name': 'Purple', 'code': 0xaf00ff},
\ {'name': 'DarkOrange3', 'code': 0xaf5f00},
\ {'name': 'IndianRed', 'code': 0xaf5f5f},
\ {'name': 'HotPink3', 'code': 0xaf5f87},
\ {'name': 'MediumOrchid3', 'code': 0xaf5faf},
\ {'name': 'MediumOrchid', 'code': 0xaf5fd7},
\ {'name': 'MediumPurple2', 'code': 0xaf5fff},
\ {'name': 'DarkGoldenrod', 'code': 0xaf8700},
\ {'name': 'LightSalmon3', 'code': 0xaf875f},
\ {'name': 'RosyBrown', 'code': 0xaf8787},
\ {'name': 'Grey63', 'code': 0xaf87af},
\ {'name': 'MediumPurple2', 'code': 0xaf87d7},
\ {'name': 'MediumPurple1', 'code': 0xaf87ff},
\ {'name': 'Gold3', 'code': 0xafaf00},
\ {'name': 'DarkKhaki', 'code': 0xafaf5f},
\ {'name': 'NavajoWhite3', 'code': 0xafaf87},
\ {'name': 'Grey69', 'code': 0xafafaf},
\ {'name': 'LightSteelBlue3', 'code': 0xafafd7},
\ {'name': 'LightSteelBlue', 'code': 0xafafff},
\ {'name': 'Yellow3', 'code': 0xafd700},
\ {'name': 'DarkOliveGreen3', 'code': 0xafd75f},
\ {'name': 'DarkSeaGreen3', 'code': 0xafd787},
\ {'name': 'DarkSeaGreen2', 'code': 0xafd7af},
\ {'name': 'LightCyan3', 'code': 0xafd7d7},
\ {'name': 'LightSkyBlue1', 'code': 0xafd7ff},
\ {'name': 'GreenYellow', 'code': 0xafff00},
\ {'name': 'DarkOliveGreen2', 'code': 0xafff5f},
\ {'name': 'PaleGreen1', 'code': 0xafff87},
\ {'name': 'DarkSeaGreen2', 'code': 0xafffaf},
\ {'name': 'DarkSeaGreen1', 'code': 0xafffd7},
\ {'name': 'PaleTurquoise1', 'code': 0xafffff},
\ {'name': 'Red3', 'code': 0xd70000},
\ {'name': 'DeepPink3', 'code': 0xd7005f},
\ {'name': 'DeepPink3', 'code': 0xd70087},
\ {'name': 'Magenta3', 'code': 0xd700af},
\ {'name': 'Magenta3', 'code': 0xd700d7},
\ {'name': 'Magenta2', 'code': 0xd700ff},
\ {'name': 'DarkOrange3', 'code': 0xd75f00},
\ {'name': 'IndianRed', 'code': 0xd75f5f},
\ {'name': 'HotPink3', 'code': 0xd75f87},
\ {'name': 'HotPink2', 'code': 0xd75faf},
\ {'name': 'Orchid', 'code': 0xd75fd7},
\ {'name': 'MediumOrchid1', 'code': 0xd75fff},
\ {'name': 'Orange3', 'code': 0xd78700},
\ {'name': 'LightSalmon3', 'code': 0xd7875f},
\ {'name': 'LightPink3', 'code': 0xd78787},
\ {'name': 'Pink3', 'code': 0xd787af},
\ {'name': 'Plum3', 'code': 0xd787d7},
\ {'name': 'Violet', 'code': 0xd787ff},
\ {'name': 'Gold3', 'code': 0xd7af00},
\ {'name': 'LightGoldenrod3', 'code': 0xd7af5f},
\ {'name': 'Tan', 'code': 0xd7af87},
\ {'name': 'MistyRose3', 'code': 0xd7afaf},
\ {'name': 'Thistle3', 'code': 0xd7afd7},
\ {'name': 'Plum2', 'code': 0xd7afff},
\ {'name': 'Yellow3', 'code': 0xd7d700},
\ {'name': 'Khaki3', 'code': 0xd7d75f},
\ {'name': 'LightGoldenrod2', 'code': 0xd7d787},
\ {'name': 'LightYellow3', 'code': 0xd7d7af},
\ {'name': 'Grey84', 'code': 0xd7d7d7},
\ {'name': 'LightSteelBlue1', 'code': 0xd7d7ff},
\ {'name': 'Yellow2', 'code': 0xd7ff00},
\ {'name': 'DarkOliveGreen1', 'code': 0xd7ff5f},
\ {'name': 'DarkOliveGreen1', 'code': 0xd7ff87},
\ {'name': 'DarkSeaGreen1', 'code': 0xd7ffaf},
\ {'name': 'Honeydew2', 'code': 0xd7ffd7},
\ {'name': 'LightCyan1', 'code': 0xd7ffff},
\ {'name': 'Red1', 'code': 0xff0000},
\ {'name': 'DeepPink2', 'code': 0xff005f},
\ {'name': 'DeepPink1', 'code': 0xff0087},
\ {'name': 'DeepPink1', 'code': 0xff00af},
\ {'name': 'Magenta2', 'code': 0xff00d7},
\ {'name': 'Magenta1', 'code': 0xff00ff},
\ {'name': 'OrangeRed1', 'code': 0xff5f00},
\ {'name': 'IndianRed1', 'code': 0xff5f5f},
\ {'name': 'IndianRed1', 'code': 0xff5f87},
\ {'name': 'HotPink', 'code': 0xff5faf},
\ {'name': 'HotPink', 'code': 0xff5fd7},
\ {'name': 'MediumOrchid1', 'code': 0xff5fff},
\ {'name': 'DarkOrange', 'code': 0xff8700},
\ {'name': 'Salmon1', 'code': 0xff875f},
\ {'name': 'LightCoral', 'code': 0xff8787},
\ {'name': 'PaleVioletRed1', 'code': 0xff87af},
\ {'name': 'Orchid2', 'code': 0xff87d7},
\ {'name': 'Orchid1', 'code': 0xff87ff},
\ {'name': 'Orange1', 'code': 0xffaf00},
\ {'name': 'SandyBrown', 'code': 0xffaf5f},
\ {'name': 'LightSalmon1', 'code': 0xffaf87},
\ {'name': 'LightPink1', 'code': 0xffafaf},
\ {'name': 'Pink1', 'code': 0xffafd7},
\ {'name': 'Plum1', 'code': 0xffafff},
\ {'name': 'Gold1', 'code': 0xffd700},
\ {'name': 'LightGoldenrod2', 'code': 0xffd75f},
\ {'name': 'LightGoldenrod2', 'code': 0xffd787},
\ {'name': 'NavajoWhite1', 'code': 0xffd7af},
\ {'name': 'MistyRose1', 'code': 0xffd7d7},
\ {'name': 'Thistle1', 'code': 0xffd7ff},
\ {'name': 'Yellow1', 'code': 0xffff00},
\ {'name': 'LightGoldenrod1', 'code': 0xffff5f},
\ {'name': 'Khaki1', 'code': 0xffff87},
\ {'name': 'Wheat1', 'code': 0xffffaf},
\ {'name': 'Cornsilk1', 'code': 0xffffd7},
\ {'name': 'Grey100', 'code': 0xffffff},
\ {'name': 'Grey3', 'code': 0x080808},
\ {'name': 'Grey7', 'code': 0x121212},
\ {'name': 'Grey11', 'code': 0x1c1c1c},
\ {'name': 'Grey15', 'code': 0x262626},
\ {'name': 'Grey19', 'code': 0x303030},
\ {'name': 'Grey23', 'code': 0x3a3a3a},
\ {'name': 'Grey27', 'code': 0x444444},
\ {'name': 'Grey30', 'code': 0x4e4e4e},
\ {'name': 'Grey35', 'code': 0x585858},
\ {'name': 'Grey39', 'code': 0x626262},
\ {'name': 'Grey42', 'code': 0x6c6c6c},
\ {'name': 'Grey46', 'code': 0x767676},
\ {'name': 'Grey50', 'code': 0x808080},
\ {'name': 'Grey54', 'code': 0x8a8a8a},
\ {'name': 'Grey58', 'code': 0x949494},
\ {'name': 'Grey62', 'code': 0x9e9e9e},
\ {'name': 'Grey66', 'code': 0xa8a8a8},
\ {'name': 'Grey70', 'code': 0xb2b2b2},
\ {'name': 'Grey74', 'code': 0xbcbcbc},
\ {'name': 'Grey78', 'code': 0xc6c6c6},
\ {'name': 'Grey82', 'code': 0xd0d0d0},
\ {'name': 'Grey85', 'code': 0xdadada},
\ {'name': 'Grey89', 'code': 0xe4e4e4},
\ {'name': 'Grey93', 'code': 0xeeeeee},
\]
" Sort by color code, and add 'xtermnr' property to each element.
function! s:new_table(ansi_colors) abort
" TODO ansi color palette
let table = deepcopy(s:color_table)
" call map(copy(a:ansi_colors), {i,c ->
"\ extend(table[i], {'code': s:parse_color(c)})
"\})
let zipped = map(table, {i,v -> [i, v]})
let sorted = sort(zipped, {a,b ->
\ a[1].code ># b[1].code ? 1 : a[1].code is# b[1].code ? 0 : -1
\})
let mapped = map(sorted, {_,tuple -> {
\ 'xtermnr' : tuple[0],
\ 'name' : tuple[1].name,
\ 'code' : tuple[1].code
\}})
return filter(mapped, {_,v -> v.xtermnr >= 16})
endfunction
let s:group_prefix = 'dupTermBuf'
function! duptermbuf#open() abort
let lines = getline(1, '$')
let maxrow = term_getsize('')[0]
let chunk_list = s:flatmap(range(1, maxrow), {row ->
\ s:scrape(term_scrape('', row), row)
\})
new
call s:add_colors(chunk_list)
call setline('.', lines)
call s:set_buffer_options()
endfunction
function! s:flatmap(list, f) abort
let result = []
return map(copy(a:list), {_,v -> extend(result, a:f(v))})[-1]
endfunction
" Convert term_scrape() return value into the regexp patterns of the row.
function! s:scrape(list, row) abort
if empty(a:list)
return []
endif
let [first; rest] = a:list
let prev_key = [first.fg, first.bg, first.attr]
let prev_scr = first
let buf = [first.chars]
let chunks = []
let start = 1
let end = start + first.width
for scr in rest
let key = [scr.fg, scr.bg, scr.attr]
if key ==# prev_key
let buf += [scr.chars]
let end += scr.width
continue
endif
let chunks += [{
\ 'col': start,
\ 'row': a:row,
\ 'scr': prev_scr,
\ 'str': join(buf, '')
\}]
let start = end
let prev_key = key
let prev_scr = scr
let buf = [scr.chars]
endfor
let chunks += [{
\ 'col': start,
\ 'row': a:row,
\ 'scr': prev_scr,
\ 'str': join(buf, '')
\}]
return chunks
endfunction
function! s:add_colors(chunk_list) abort
call writefile(split(PP(a:chunk_list), '\n'), 'dump.txt')
let colors = s:generate_colors(a:chunk_list)
let highlights = s:generate_highlights(a:chunk_list)
for h in highlights
execute 'hi' h.group join(h.args, ' ')
endfor
for c in colors
call matchadd(c.group, c.pattern, c.priority, c.id, c.dict)
endfor
endfunction
function! s:generate_colors(chunk_list) abort
return map(copy(a:chunk_list), {i,c ->
\ {
\ 'group': s:group_prefix . 'Row' . c.row . 'Chunk' . i,
\ 'pattern': '\m' . '\%' . c.row . 'l\%' . c.col . 'c\V' . c.str,
\ 'priority': 10,
\ 'id': -1,
\ 'dict': {},
\ }
\})
endfunction
function! s:generate_highlights(chunk_list) abort
" XXX skip 0 - 15 system palette
let table = s:new_table(term_getansicolors(''))
return map(copy(a:chunk_list), {i,c ->
\ {
\ 'group': s:group_prefix . 'Row' . c.row . 'Chunk' . i,
\ 'args': s:to_hicmd_args(c.scr, table)
\ }
\})
endfunction
" This returns {args} of ':hi {group} {args}'
function! s:to_hicmd_args(scr, table) abort
let fg = s:pick_near_term_color(a:scr.fg, a:table)
let bg = s:pick_near_term_color(a:scr.bg, a:table)
let args = []
if !empty(fg)
let args += ['ctermfg=' . fg.xtermnr]
endif
if !empty(bg)
let args += ['ctermbg=' . bg.xtermnr]
endif
let args += [
\ 'guifg=' . a:scr.fg,
\ 'guibg=' . a:scr.bg,
\]
let args += s:to_attr_list(a:scr.attr)
return args
endfunction
" :h cterm-colors
" Convert color to cterm-colors.
function! s:pick_near_term_color(color, table) abort
let code = s:parse_color(a:color)
if code < 0
return {}
endif
" XXX table may contain duplicate codes... but is it problem?
let [index, exact] = s:bsearch_code(a:table, code)
if exact
return a:table[index]
endif
if index <= 0
return a:table[0]
endif
if index >= len(a:table) - 1
return a:table[-1]
endif
let ldiff = abs(code - a:table[index].code)
let rdiff = abs(code - a:table[index + 1].code)
return a:table[ldiff < rdiff ? index : index + 1]
endfunction
" TODO support color name (:h gui-colors)
function! s:parse_color(color) abort
let m = matchlist(a:color, '^#\(\x\x\)\(\x\x\)\(\x\x\)$')
if empty(m)
return -1
endif
return s:hex3_to_code(m[1], m[2], m[3])
endfunction
function! s:bsearch_code(list, code) abort
let [min, max] = [0, len(a:list)]
while min <=# max
let mid = min + (max - min) / 2
if a:list[mid].code <# a:code
let min = mid + 1
elseif a:list[mid].code ># a:code
let max = mid - 1
else
return [mid, v:true]
endif
endwhile
return [mid, v:false]
endfunction
" r = '12', g = '34', b = '56' -> 0x123456
function! s:hex3_to_code(r, g, b) abort
return +printf('%d', +printf('0x%02s%02s%02s', a:r, a:g, a:b))
endfunction
" 0x123456 -> '1234ff'
function! s:code_to_hex(code) abort
return printf('%06x', a:code)
endfunction
" [{what}, {attr}]
" {what} = see :help term_getattr()
" {attr} = see :help attr-list
let s:term_attr_list = [
\ ['bold', 'bold'],
\ ['italic', 'italic'],
\ ['underline', 'underline'],
\ ['strike', 'strikethrough'],
\ ['reverse', 'reverse'],
\]
" :help attr-list
function! s:to_attr_list(attr) abort
let list = map(copy(s:term_attr_list), {_,a ->
\ term_getattr(a:attr, a[0]) ? a[1] : ''
\})
call filter(list, {_,v -> !empty(v)})
if empty(list)
return []
endif
let csv = join(list, ',')
return ['term=' . csv, 'gui=' . csv]
endfunction
function! s:set_buffer_options() abort
setlocal readonly buftype=nofile nolist
augroup dup-termbuf
autocmd!
autocmd FileChangedRO <buffer> call clearmatches()
augroup END
endfunction
function! s:test() abort
let table = s:new_table(term_getansicolors(''))
let [index, exact] = s:bsearch_code(table, 0x40ff40)
let picked = s:pick_near_term_color('#40ff40', table)
PP [index, exact, table[index], picked]
endfunction
call s:test()
let &cpo = s:save_cpo
unlet s:save_cpo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment