@@ -8,7 +8,6 @@ import { FontPresetsService } from '../../services/font-presets.service';
88 imports : [ CommonModule ] ,
99 template : `
1010 <div class="flex-1 overflow-hidden flex flex-col rounded-lg" [ngStyle]="windowStyles()">
11- <!-- Title bar -->
1211 @if (!hideDecorations()) {
1312 <div class="flex items-center px-3 py-2 select-none border-b"
1413 [style.background]="titleBarBg()"
@@ -20,30 +19,16 @@ import { FontPresetsService } from '../../services/font-presets.service';
2019 </div>
2120 <span class="flex-1 text-center text-xs select-none opacity-50"
2221 [style.font-family]="terminalFont()"
23- [style.color]="colors().foreground">
24- bash
25- </span>
22+ [style.color]="colors().foreground">bash</span>
2623 </div>
2724 }
2825
29- <!-- Tab bar (top) -->
3026 @if (tabBarVisible() && tabBarEdge() === 'top') {
31- <div [ngStyle]="tabBarStyles()">
32- @for (tab of tabs; track tab.name; let i = $index) {
33- <div class="px-3 py-1 text-xs whitespace-nowrap"
34- [ngStyle]="tabStyles(i === 0)">
35- {{ tab.name }}
36- </div>
37- }
38- </div>
27+ <ng-container *ngTemplateOutlet="tabBar"></ng-container>
3928 }
4029
41- <!-- Terminal content -->
4230 <div class="flex-1 overflow-hidden relative p-3" [ngStyle]="terminalStyles()">
43- <!-- Background with opacity -->
4431 <div class="absolute inset-0 -z-10" [ngStyle]="bgStyles()"></div>
45-
46- <!-- Content lines -->
4732 <div class="space-y-0.5" [style.line-height]="'1.4'">
4833 <div class="whitespace-nowrap">
4934 <span [style.color]="c('color2')">user</span>
@@ -69,68 +54,90 @@ import { FontPresetsService } from '../../services/font-presets.service';
6954 <span [style.color]="c('foreground')">cat </span>
7055 <span class="url-text" [style.color]="urlColor()" [style.text-decoration]="urlUnderline()">https://sw.kovidgoyal.net/kitty/</span>
7156 </div>
72-
73- <!-- Cursor -->
7457 <div class="whitespace-nowrap mt-2">
7558 <span [style.color]="c('color2')">user</span>
7659 <span [style.color]="c('foreground')" class="opacity-60">@</span>
7760 <span [style.color]="c('color4')">arch</span>
7861 <span [style.color]="c('foreground')" class="opacity-60">:~$ </span>
7962 @if (cursorShape() === 'block') {
8063 <span class="inline-block w-2 h-4 align-text-bottom"
81- [class.animate-pulse ]="cursorBlinking()"
64+ [class.animate-blink ]="cursorBlinking()"
8265 [style.background]="cursorColor()"
8366 [style.color]="cursorTextColor()"> </span>
8467 } @else if (cursorShape() === 'beam') {
8568 <span class="inline-block w-0.5 h-4 align-text-bottom border-l-2"
86- [class.animate-pulse ]="cursorBlinking()"
69+ [class.animate-blink ]="cursorBlinking()"
8770 [style.border-color]="cursorColor()"></span>
8871 } @else {
8972 <span class="inline-block w-2 h-4 align-text-bottom border-b-2"
90- [class.animate-pulse ]="cursorBlinking()"
73+ [class.animate-blink ]="cursorBlinking()"
9174 [style.border-color]="cursorColor()"> </span>
9275 }
9376 </div>
9477 </div>
9578 </div>
9679
97- <!-- Tab bar (bottom) -->
9880 @if (tabBarVisible() && tabBarEdge() === 'bottom') {
99- <div [ngStyle]="tabBarStyles()">
100- @for (tab of tabs; track tab.name; let i = $index) {
101- <div class="px-3 py-1 text-xs whitespace-nowrap"
102- [ngStyle]="tabStyles(i === 0)">
103- {{ tab.name }}
104- </div>
105- }
106- </div>
81+ <ng-container *ngTemplateOutlet="tabBar"></ng-container>
10782 }
10883 </div>
84+
85+ <ng-template #tabBar>
86+ <div class="flex items-stretch overflow-hidden select-none" [ngStyle]="tabBarContainerStyles()">
87+ @switch (tabBarStyle()) {
88+ @case ('fade') {
89+ @for (tab of tabs; track tab.name; let i = $index; let first = $first) {
90+ @if (!first) { <div class="w-px" [style.background]="tabBarBg()"></div> }
91+ <div class="px-3 py-1.5 text-xs whitespace-nowrap flex items-center" [ngStyle]="fadeTabStyles(i === 0, i)">{{ tab.name }}</div>
92+ }
93+ <div class="flex-1" [style.background]="tabBarBg()"></div>
94+ }
95+ @case ('separator') {
96+ @for (tab of tabs; track tab.name; let i = $index; let first = $first) {
97+ @if (!first) {
98+ <div class="px-1 py-1.5 text-xs flex items-center opacity-50" [style.color]="inactiveTabColor()">{{ tabSeparator() }}</div>
99+ }
100+ <div class="px-3 py-1.5 text-xs whitespace-nowrap flex items-center" [ngStyle]="simpleTabStyles(i === 0)">{{ tab.name }}</div>
101+ }
102+ <div class="flex-1" [style.background]="tabBarBg()"></div>
103+ }
104+ @case ('slant') {
105+ @for (tab of tabs; track tab.name; let i = $index) {
106+ <div class="relative px-4 py-1.5 text-xs whitespace-nowrap flex items-center slanted-tab" [ngStyle]="slantTabStyles(i === 0)">{{ tab.name }}</div>
107+ }
108+ <div class="flex-1" [style.background]="tabBarBg()"></div>
109+ }
110+ @case ('powerline') {
111+ @for (tab of tabs; track tab.name; let i = $index) {
112+ <div class="relative px-3 py-1.5 text-xs whitespace-nowrap flex items-center powerline-tab" [ngStyle]="powerlineTabStyles(i === 0)">{{ tab.name }}</div>
113+ }
114+ <div class="flex-1" [style.background]="tabBarBg()"></div>
115+ }
116+ @default {
117+ @for (tab of tabs; track tab.name; let i = $index) {
118+ <div class="px-3 py-1.5 text-xs whitespace-nowrap flex items-center" [ngStyle]="simpleTabStyles(i === 0)">{{ tab.name }}</div>
119+ }
120+ <div class="flex-1" [style.background]="tabBarBg()"></div>
121+ }
122+ }
123+ </div>
124+ </ng-template>
109125 ` ,
110126 styles : [ `
111- .url-text {
112- text-decoration: underline;
113- cursor: pointer;
114- }
115- @keyframes blink {
116- 0%, 50% { opacity: 1; }
117- 51%, 100% { opacity: 0; }
118- }
119- .animate-pulse {
120- animation: blink 1s step-end infinite;
121- }
127+ .url-text { text-decoration: underline; cursor: pointer; }
128+ @keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
129+ .animate-blink { animation: blink 1s step-end infinite; }
130+ .slanted-tab { clip-path: polygon(8px 0%, 100% 0%, calc(100% - 8px) 100%, 0% 100%); padding-left: 16px; padding-right: 16px; margin-right: -4px; }
131+ .slanted-tab:first-child { clip-path: polygon(0% 0%, 100% 0%, calc(100% - 8px) 100%, 0% 100%); padding-left: 12px; }
132+ .powerline-tab { padding-right: 16px; }
133+ .powerline-tab::after { content: ''; position: absolute; right: -8px; top: 0; width: 0; height: 0; border-top: 12px solid transparent; border-bottom: 12px solid transparent; border-left: 8px solid var(--tab-bg); z-index: 1; }
122134 ` ]
123135} )
124136export class TerminalWindowComponent {
125137 private readonly store = inject ( ConfigStoreService ) ;
126138 private readonly fontPresets = inject ( FontPresetsService ) ;
127139
128- // Fixed 3 tabs for preview
129- readonly tabs = [
130- { name : 'bash' } ,
131- { name : 'nvim' } ,
132- { name : 'git' }
133- ] ;
140+ readonly tabs = [ { name : 'bash' } , { name : 'nvim' } , { name : 'git' } ] ;
134141
135142 constructor ( ) {
136143 effect ( ( ) => {
@@ -146,137 +153,70 @@ export class TerminalWindowComponent {
146153 windowCfg = computed ( ( ) => this . store . configState ( ) . window_layout ) ;
147154 mouseCfg = computed ( ( ) => this . store . configState ( ) . mouse ) ;
148155
149- c ( key : string ) : string {
150- return ( this . colors ( ) as unknown as Record < string , string > ) [ key ] || '#888888' ;
151- }
156+ c ( key : string ) : string { return ( this . colors ( ) as unknown as Record < string , string > ) [ key ] || '#888888' ; }
152157
153- terminalFont = computed ( ( ) => {
154- const f = this . fonts ( ) . font_family ;
155- return `"${ f } ", "JetBrains Mono", monospace` ;
156- } ) ;
158+ terminalFont = computed ( ( ) => { const f = this . fonts ( ) . font_family ; return `"${ f } ", "JetBrains Mono", monospace` ; } ) ;
157159
158160 windowStyles = computed ( ( ) => {
159161 const cfg = this . windowCfg ( ) ;
160162 const borderWidth = this . parseBorderWidth ( cfg . window_border_width ) ;
161- const styles : Record < string , string > = {
162- background : this . colors ( ) . background ,
163- border : borderWidth === '0px' ? '1px solid ' + this . borderColor ( ) : `${ borderWidth } solid ${ cfg . active_border_color } ` ,
164- } ;
165- return styles ;
163+ return { background : this . colors ( ) . background , border : borderWidth === '0px' ? '1px solid ' + this . borderColor ( ) : `${ borderWidth } solid ${ cfg . active_border_color } ` } ;
166164 } ) ;
167165
168- titleBarBg = computed ( ( ) => {
169- const bg = this . colors ( ) . background ;
170- return this . darken ( bg , 10 ) ;
171- } ) ;
172-
173- borderColor = computed ( ( ) => {
174- const bg = this . colors ( ) . background ;
175- return this . darken ( bg , 20 ) ;
176- } ) ;
166+ titleBarBg = computed ( ( ) => this . darken ( this . colors ( ) . background , 10 ) ) ;
167+ borderColor = computed ( ( ) => this . darken ( this . colors ( ) . background , 20 ) ) ;
168+ tabBarBg = computed ( ( ) => { const cfg = this . tabBarCfg ( ) ; return ( cfg . tab_bar_background && cfg . tab_bar_background !== 'none' ) ? cfg . tab_bar_background : this . darken ( this . colors ( ) . background , 8 ) ; } ) ;
177169
178170 hideDecorations = computed ( ( ) => this . windowCfg ( ) . hide_window_decorations ) ;
179171
180172 terminalStyles = computed ( ( ) => {
181173 const cfg = this . windowCfg ( ) ;
182174 const padding = Math . min ( Math . max ( cfg . window_padding_width || 0 , 0 ) , 32 ) ;
183175 const fontSize = Math . max ( 6 , Math . min ( this . fonts ( ) . font_size , 24 ) ) ;
184-
185- return {
186- fontFamily : this . terminalFont ( ) ,
187- fontSize : `${ fontSize } px` ,
188- color : this . colors ( ) . foreground ,
189- padding : `${ padding } px` ,
190- } ;
176+ return { fontFamily : this . terminalFont ( ) , fontSize : `${ fontSize } px` , color : this . colors ( ) . foreground , padding : `${ padding } px` } ;
191177 } ) ;
192178
193- bgStyles = computed ( ( ) => {
194- const bg = this . colors ( ) . background ;
195- const opacity = this . colors ( ) . background_opacity ;
196- const clamped = typeof opacity === 'number' ? Math . max ( 0.05 , Math . min ( 1 , opacity ) ) : 1 ;
179+ bgStyles = computed ( ( ) => { const bg = this . colors ( ) . background ; const opacity = this . colors ( ) . background_opacity ; const clamped = typeof opacity === 'number' ? Math . max ( 0.05 , Math . min ( 1 , opacity ) ) : 1 ; return { background : bg , opacity : String ( clamped ) } ; } ) ;
197180
198- return {
199- background : bg ,
200- opacity : String ( clamped ) ,
201- } ;
202- } ) ;
181+ tabBarVisible = computed ( ( ) => { const cfg = this . tabBarCfg ( ) ; const minTabs = cfg . tab_bar_min_tabs ?? 2 ; return cfg . tab_bar_style !== 'hidden' && 3 >= minTabs ; } ) ;
182+ tabBarEdge = computed ( ( ) => this . tabBarCfg ( ) . tab_bar_edge ) ;
183+ tabBarStyle = computed ( ( ) => this . tabBarCfg ( ) . tab_bar_style ) ;
184+ tabSeparator = computed ( ( ) => this . tabBarCfg ( ) . tab_separator || ' ┇' ) ;
203185
204- tabBarVisible = computed ( ( ) => {
205- const cfg = this . tabBarCfg ( ) ;
206- const minTabs = cfg . tab_bar_min_tabs ?? 2 ;
207- return cfg . tab_bar_style !== 'hidden' && 3 >= minTabs ;
208- } ) ;
186+ tabBarContainerStyles = computed ( ( ) => ( { background : this . tabBarBg ( ) , borderTop : this . tabBarEdge ( ) === 'bottom' ? `1px solid ${ this . borderColor ( ) } ` : 'none' , borderBottom : this . tabBarEdge ( ) === 'top' ? `1px solid ${ this . borderColor ( ) } ` : 'none' } ) ) ;
187+ inactiveTabColor = computed ( ( ) => this . tabBarCfg ( ) . inactive_tab_foreground ) ;
209188
210- tabBarEdge = computed ( ( ) => this . tabBarCfg ( ) . tab_bar_edge ) ;
189+ simpleTabStyles ( isActive : boolean ) : Record < string , string > {
190+ const cfg = this . tabBarCfg ( ) ;
191+ return isActive ? { background : cfg . active_tab_background , color : cfg . active_tab_foreground , fontWeight : '600' } : { background : cfg . inactive_tab_background , color : cfg . inactive_tab_foreground } ;
192+ }
211193
212- tabBarStyles = computed ( ( ) = > {
194+ fadeTabStyles ( isActive : boolean , index : number ) : Record < string , string > {
213195 const cfg = this . tabBarCfg ( ) ;
214- const bg = cfg . tab_bar_background === 'none' || ! cfg . tab_bar_background
215- ? this . darken ( this . colors ( ) . background , 8 )
216- : cfg . tab_bar_background ;
196+ const fade = cfg . tab_fade || [ 0.25 , 0.5 , 0.75 , 1 ] ;
197+ const fadeVal = isActive ? 1 : ( fade [ Math . min ( index , fade . length - 1 ) ] ?? 1 ) ;
198+ return isActive ? { background : cfg . active_tab_background , color : cfg . active_tab_foreground , fontWeight : '600' } : { background : cfg . inactive_tab_background , color : cfg . inactive_tab_foreground , opacity : String ( fadeVal ) } ;
199+ }
217200
218- return {
219- display : 'flex' ,
220- background : bg ,
221- borderTop : this . tabBarEdge ( ) === 'bottom' ? 'none' : `1px solid ${ this . borderColor ( ) } ` ,
222- borderBottom : this . tabBarEdge ( ) === 'top' ? 'none' : `1px solid ${ this . borderColor ( ) } ` ,
223- } ;
224- } ) ;
201+ slantTabStyles ( isActive : boolean ) : Record < string , string > {
202+ const cfg = this . tabBarCfg ( ) ;
203+ return isActive ? { background : cfg . active_tab_background , color : cfg . active_tab_foreground , fontWeight : '600' } : { background : cfg . inactive_tab_background , color : cfg . inactive_tab_foreground } ;
204+ }
225205
226- tabStyles ( isActive : boolean ) : Record < string , string > {
206+ powerlineTabStyles ( isActive : boolean ) : Record < string , string > {
227207 const cfg = this . tabBarCfg ( ) ;
228- if ( isActive ) {
229- return {
230- background : cfg . active_tab_background ,
231- color : cfg . active_tab_foreground ,
232- fontWeight : '600' ,
233- } ;
234- }
235- return {
236- background : cfg . inactive_tab_background ,
237- color : cfg . inactive_tab_foreground ,
238- opacity : '0.85' ,
239- } ;
208+ const bg = isActive ? cfg . active_tab_background : cfg . inactive_tab_background ;
209+ return { background : bg , color : isActive ? cfg . active_tab_foreground : cfg . inactive_tab_foreground , fontWeight : isActive ? '600' : '400' , '--tab-bg' : bg } as Record < string , string > ;
240210 }
241211
242212 cursorShape = computed ( ( ) => this . cursorCfg ( ) . cursor_shape ) ;
243- cursorColor = computed ( ( ) => {
244- const c = this . cursorCfg ( ) . cursor ;
245- return c === 'none' || ! c ?. startsWith ( '#' ) ? this . colors ( ) . foreground : c ;
246- } ) ;
247- cursorTextColor = computed ( ( ) => {
248- const c = this . cursorCfg ( ) . cursor_text_color ;
249- return c === 'background' ? this . colors ( ) . background : ( c ?. startsWith ( '#' ) ? c : this . colors ( ) . background ) ;
250- } ) ;
213+ cursorColor = computed ( ( ) => { const c = this . cursorCfg ( ) . cursor ; return c === 'none' || ! c ?. startsWith ( '#' ) ? this . colors ( ) . foreground : c ; } ) ;
214+ cursorTextColor = computed ( ( ) => { const c = this . cursorCfg ( ) . cursor_text_color ; return c === 'background' ? this . colors ( ) . background : ( c ?. startsWith ( '#' ) ? c : this . colors ( ) . background ) ; } ) ;
251215 cursorBlinking = computed ( ( ) => this . cursorCfg ( ) . cursor_blink_interval !== 0 ) ;
252216
253217 urlColor = computed ( ( ) => this . mouseCfg ( ) . url_color || this . colors ( ) . color4 || '#0087bd' ) ;
254- urlUnderline = computed ( ( ) => {
255- const style = this . mouseCfg ( ) . url_style ;
256- if ( style === 'none' ) return 'none' ;
257- if ( style === 'double' ) return 'underline double' ;
258- if ( style === 'curly' ) return 'underline wavy' ;
259- if ( style === 'dashed' ) return 'underline dashed' ;
260- if ( style === 'dotted' ) return 'underline dotted' ;
261- return 'underline' ;
262- } ) ;
218+ urlUnderline = computed ( ( ) => { const style = this . mouseCfg ( ) . url_style ; if ( style === 'none' ) return 'none' ; if ( style === 'double' ) return 'underline double' ; if ( style === 'curly' ) return 'underline wavy' ; if ( style === 'dashed' ) return 'underline dashed' ; if ( style === 'dotted' ) return 'underline dotted' ; return 'underline' ; } ) ;
263219
264- private parseBorderWidth ( val : string ) : string {
265- if ( ! val || val === '0' || val === '0pt' || val === '0px' ) return '0px' ;
266- if ( val . endsWith ( 'pt' ) ) {
267- const n = Number . parseFloat ( val ) ;
268- return `${ Math . max ( 0 , Math . round ( n * 1.333 ) ) } px` ;
269- }
270- return val . endsWith ( 'px' ) ? val : '1px' ;
271- }
272-
273- private darken ( hex : string , amount : number ) : string {
274- if ( ! hex ?. startsWith ( '#' ) ) return '#333' ;
275- const c = hex . slice ( 1 ) ;
276- if ( c . length !== 6 ) return hex ;
277- const r = Math . max ( 0 , Math . min ( 255 , Number . parseInt ( c . slice ( 0 , 2 ) , 16 ) - amount ) ) ;
278- const g = Math . max ( 0 , Math . min ( 255 , Number . parseInt ( c . slice ( 2 , 4 ) , 16 ) - amount ) ) ;
279- const b = Math . max ( 0 , Math . min ( 255 , Number . parseInt ( c . slice ( 4 , 6 ) , 16 ) - amount ) ) ;
280- return `#${ r . toString ( 16 ) . padStart ( 2 , '0' ) } ${ g . toString ( 16 ) . padStart ( 2 , '0' ) } ${ b . toString ( 16 ) . padStart ( 2 , '0' ) } ` ;
281- }
220+ private parseBorderWidth ( val : string ) : string { if ( ! val || val === '0' || val === '0pt' || val === '0px' ) return '0px' ; if ( val . endsWith ( 'pt' ) ) { const n = Number . parseFloat ( val ) ; return `${ Math . max ( 0 , Math . round ( n * 1.333 ) ) } px` ; } return val . endsWith ( 'px' ) ? val : '1px' ; }
221+ private darken ( hex : string , amount : number ) : string { if ( ! hex ?. startsWith ( '#' ) ) return '#333' ; const c = hex . slice ( 1 ) ; if ( c . length !== 6 ) return hex ; const r = Math . max ( 0 , Math . min ( 255 , Number . parseInt ( c . slice ( 0 , 2 ) , 16 ) - amount ) ) ; const g = Math . max ( 0 , Math . min ( 255 , Number . parseInt ( c . slice ( 2 , 4 ) , 16 ) - amount ) ) ; const b = Math . max ( 0 , Math . min ( 255 , Number . parseInt ( c . slice ( 4 , 6 ) , 16 ) - amount ) ) ; return `#${ r . toString ( 16 ) . padStart ( 2 , '0' ) } ${ g . toString ( 16 ) . padStart ( 2 , '0' ) } ${ b . toString ( 16 ) . padStart ( 2 , '0' ) } ` ; }
282222}
0 commit comments