Skip to content

Commit

Permalink
Feature: Custom Titlebar (#879)
Browse files Browse the repository at this point in the history
* Update model / title

* Title parsing

* test case

* model + tests

* Get tests green

* Formatting

* Add window.title configuration

* Stub out more title pieces

* Fix build

* Wire up titlebar

* Start populating some variables

* Add workspace model

* Update workspace to include rootName, use for title

* Formatting

* Use setTitle API

* Fix warning

* Fix warnings

* Formatting
  • Loading branch information
bryphe authored Oct 21, 2019
1 parent 06a460a commit d99801e
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 0 deletions.
5 changes: 5 additions & 0 deletions integration_test/lib/Oni_IntegrationTestLib.re
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ type testCallback =
let _currentClipboard: ref(option(string)) = ref(None);
let _currentTime: ref(float) = ref(0.0);
let _currentZoom: ref(float) = ref(1.0);
let _currentTitle: ref(string) = ref("");

let setClipboard = v => _currentClipboard := v;
let getClipboard = () => _currentClipboard^;

let setTime = v => _currentTime := v;
let getTime = () => _currentTime^;

let setTitle = title => _currentTitle := title;
let getTitle = () => _currentTitle^;

let setZoom = v => _currentZoom := v;
let getZoom = () => _currentZoom^;

Expand Down Expand Up @@ -70,6 +74,7 @@ let runTest =
~setClipboardText=text => setClipboard(Some(text)),
~getScaleFactor,
~getTime,
~setTitle,
~getZoom,
~setZoom,
~executingDirectory=Revery.Environment.getExecutingDirectory(),
Expand Down
1 change: 1 addition & 0 deletions src/Core/ConfigurationParser.re
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ let configurationParsers: list(configurationTuple) = [
),
("editor.rulers", (s, v) => {...s, editorRulers: parseIntList(v)}),
("files.exclude", (s, v) => {...s, filesExclude: parseStringList(v)}),
("window.title", (s, v) => {...s, windowTitle: parseString(v)}),
(
"workbench.activityBar.visible",
(s, v) => {...s, workbenchActivityBarVisible: parseBool(v)},
Expand Down
2 changes: 2 additions & 0 deletions src/Core/ConfigurationValues.re
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type t = {
vimUseSystemClipboard,
uiShadows: bool,
uiZoom: float,
windowTitle: string,
zenModeHideTabs: bool,
zenModeSingleFile: bool,
// Experimental feature flags
Expand Down Expand Up @@ -96,6 +97,7 @@ let default = {
delete: false,
paste: false,
},
windowTitle: "${dirty}${activeEditorShort}${separator}${rootName}${separator}${appName}",
zenModeHideTabs: true,
zenModeSingleFile: true,

Expand Down
1 change: 1 addition & 0 deletions src/Model/Actions.re
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type t =
| WildmenuSelect
| WildmenuHide
| WindowSetActive(int, int)
| WindowTitleSet(string)
| WindowTreeSetSize(int, int)
| EditorGroupAdd(editorGroup)
| EditorGroupSetSize(int, EditorSize.t)
Expand Down
1 change: 1 addition & 0 deletions src/Model/Oni_Model.re
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module StatusBarModel = StatusBarModel;
module State = State;
module SyntaxHighlighting = SyntaxHighlighting;
module ThemeInfo = ThemeInfo;
module Title = Title;
module WhitespaceTokenFilter = WhitespaceTokenFilter;
module Wildmenu = Wildmenu;
module WindowManager = WindowManager;
Expand Down
1 change: 1 addition & 0 deletions src/Model/Reducer.re
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let reduce: (State.t, Actions.t) => State.t =
statusBar: StatusBarReducer.reduce(s.statusBar, a),
fileExplorer: FileExplorer.reduce(s.fileExplorer, a),
notifications: Notifications.reduce(s.notifications, a),
workspace: Workspace.reduce(s.workspace, a),
};

switch (a) {
Expand Down
5 changes: 5 additions & 0 deletions src/Model/State.re
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type t = {
statusBar: StatusBarModel.t,
windowManager: WindowManager.t,
fileExplorer: FileExplorer.t,
// [windowTitle] is the title of the window
windowTitle: string,
workspace: Workspace.t,
zenMode: bool,
// [darkMode] describes if the UI is in 'dark' or 'light' mode.
// Generally controlled by the theme.
Expand Down Expand Up @@ -76,6 +79,8 @@ let create: unit => t =
searchHighlights: SearchHighlights.create(),
statusBar: StatusBarModel.create(),
windowManager: WindowManager.create(),
windowTitle: "",
workspace: Workspace.empty,
fileExplorer: FileExplorer.create(),
zenMode: false,
darkMode: true,
Expand Down
78 changes: 78 additions & 0 deletions src/Model/Title.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Title.re
*
* Model for working with the window title
*/

open Oni_Core;

type titleSections =
| Text(string)
| Separator
| Variable(string);

type t = list(titleSections);

let regexp = Str.regexp("\\${\\([a-zA-Z0-9]+\\)}");

let ofString = str => {
let idx = ref(0);
let len = String.length(str);
let result = ref([]);
while (idx^ < len) {
switch (Str.search_forward(regexp, str, idx^)) {
| exception Not_found =>
result := [Text(String.sub(str, idx^, len - idx^)), ...result^];
idx := len;
| v =>
let prev = v - idx^;

if (prev > 0) {
result := [Text(String.sub(str, idx^, prev)), ...result^];
};

let group = Str.matched_group(1, str);
idx := v + String.length(group) + 3;
if (String.equal(group, "separator")) {
result := [Separator, ...result^];
} else {
result := [Variable(group), ...result^];
};
};
};

result^ |> List.rev;
};

let _resolve = (v: t, items: StringMap.t(string)) => {
let f = section => {
switch (section) {
| Text(sz) => Some(Text(sz))
| Separator => Some(Separator)
| Variable(sz) =>
switch (StringMap.find_opt(sz, items)) {
| Some(v) => Some(Text(v))
| None => None
}
};
};

v |> List.map(f) |> Utility.filterMap(v => v);
};

let toString = (v: t, items: StringMap.t(string)) => {
let resolvedItems = _resolve(v, items);

let rec f = (v: t, hadText) => {
switch (v) {
| [Separator, Text(t2), ...tail] when hadText =>
" - " ++ t2 ++ f(tail, true)
| [Text(t), ...tail] => t ++ f(tail, true)
| [Separator, ...tail] => f(tail, false)
| [Variable(_v), ...tail] => f(tail, false)
| [] => ""
};
};

f(resolvedItems, false);
};
26 changes: 26 additions & 0 deletions src/Model/Workspace.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* A workspace models the current 'ambient environment' of the editor, in particular:
* - Current directory
*
* ...and eventually
* - Open editors
* - Local modifications (hot exit)
* - Per-workspace configuration
*/

type workspace = {
workingDirectory: string,
rootName: string,
};

type t = option(workspace);

let empty: t = None;

let reduce = (v: t, a) => {
switch (a) {
| Actions.OpenExplorer(dir) =>
Some({workingDirectory: dir, rootName: Filename.basename(dir)})
| _ => v
};
};
4 changes: 4 additions & 0 deletions src/Store/StoreThread.re
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ let start =
~setZoom,
~quit,
~getTime,
~setTitle,
~window: option(Revery.Window.t),
~cliOptions: option(Oni_Core.Cli.t),
~getScaleFactor,
Expand Down Expand Up @@ -143,6 +144,8 @@ let start =
let inputStream =
InputStoreConnector.start(getState, window, runRunEffects);

let titleUpdater = TitleStoreConnector.start(setTitle);

let (storeDispatch, storeStream) =
Isolinear.Store.create(
~initialState=state,
Expand All @@ -167,6 +170,7 @@ let start =
acpUpdater,
hoverUpdater,
completionUpdater,
titleUpdater,
]),
(),
);
Expand Down
77 changes: 77 additions & 0 deletions src/Store/TitleStoreConnector.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* TitleStoreConnector
*
* This implements an updater (reducer + side effects) for the window title
*/

module Core = Oni_Core;
module Model = Oni_Model;

module Actions = Model.Actions;

let start = setTitle => {
let _lastTitle = ref("");

let getTemplateVariables: Model.State.t => Core.StringMap.t(string) =
state => {
let initialValues = [("appName", "Onivim 2")];

let initialValues =
switch (Model.Selectors.getActiveBuffer(state)) {
| None => initialValues
| Some(buf) =>
let fp = Model.Buffer.getFilePath(buf);
let ret =
switch (fp) {
| None => initialValues
| Some(fp) =>
let activeEditorShort = Filename.basename(fp);
[("activeEditorShort", activeEditorShort), ...initialValues];
};
switch (Model.Buffer.isModified(buf)) {
| false => ret
| true => [("dirty", "*"), ...ret]
};
};

let initialValues =
switch (state.workspace) {
| None => initialValues
| Some(workspace) => [
("rootName", workspace.rootName),
...initialValues,
]
};

initialValues |> List.to_seq |> Core.StringMap.of_seq;
};

let updateTitleEffect = state =>
Isolinear.Effect.createWithDispatch(~name="title.update", _dispatch => {
let templateVariables = getTemplateVariables(state);
let titleTemplate =
Core.Configuration.getValue(c => c.windowTitle, state.configuration);

let titleModel = Model.Title.ofString(titleTemplate);
let title = Model.Title.toString(titleModel, templateVariables);
print_endline("!!! got title: " ++ title);

if (!String.equal(_lastTitle^, title)) {
print_endline("!!! TITLE: " ++ title);
_lastTitle := title;
setTitle(title);
};
});

let updater = (state: Model.State.t, action: Actions.t) => {
switch (action) {
| Init => (state, updateTitleEffect(state))
| BufferEnter(_) => (state, updateTitleEffect(state))
| BufferSetModified(_) => (state, updateTitleEffect(state))
// Catch directory changes
| OpenExplorer(_) => (state, updateTitleEffect(state))
| _ => (state, Isolinear.Effect.none)
};
};
updater;
};
5 changes: 5 additions & 0 deletions src/bin_editor/Oni2_editor.re
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ let init = app => {

let setZoom = zoomFactor => Window.setZoom(w, zoomFactor);

let setTitle = title => {
Window.setTitle(w, title);
};

let quit = code => {
App.quit(~code, app);
};
Expand All @@ -90,6 +94,7 @@ let init = app => {
~getScaleFactor,
~getZoom,
~setZoom,
~setTitle,
~window=Some(w),
~cliOptions=Some(cliOptions),
~quit,
Expand Down
46 changes: 46 additions & 0 deletions test/Model/TitleTests.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
open TestFramework;

open Oni_Core;

module Title = Oni_Model.Title;

describe("Title", ({describe, _}) => {
describe("parse", ({test, _}) => {
test("plain string", ({expect}) => {
let title = Title.ofString("abc");
expect.equal(title, [Title.Text("abc")]);
});
test("string with separator", ({expect}) => {
let title = Title.ofString("abc${separator}def");
expect.equal(
title,
[Title.Text("abc"), Title.Separator, Title.Text("def")],
);
});
test("string with variables", ({expect}) => {
let title = Title.ofString("${variable1}${separator}${variable2}");
expect.equal(
title,
[
Title.Variable("variable1"),
Title.Separator,
Title.Variable("variable2"),
],
);
});
});

describe("toString", ({test, _}) => {
let simpleMap =
[("variable1", "rv1"), ("variable2", "rv2")]
|> List.to_seq
|> StringMap.of_seq;

test("basic case", ({expect}) => {
let title =
Title.ofString("prefix${variable1}${separator}${variable2}postfix");
let result = Title.toString(title, simpleMap);
expect.string(result).toEqual("prefixrv1 - rv2postfix");
});
});
});

0 comments on commit d99801e

Please sign in to comment.